32141 lines
1.2 MiB
Plaintext
32141 lines
1.2 MiB
Plaintext
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import fcntl
|
|
import hashlib
|
|
import json
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
import threading
|
|
import time
|
|
from collections.abc import AsyncIterator, Callable, Iterator, Mapping
|
|
from contextlib import asynccontextmanager, contextmanager
|
|
from dataclasses import dataclass, replace
|
|
from datetime import UTC, datetime, timedelta
|
|
from itertools import count
|
|
from pathlib import Path
|
|
from types import MethodType, SimpleNamespace
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from missioncore_plugin_sdk.v0alpha2 import RuntimeActionInvocation
|
|
from pydantic import SecretStr, ValidationError
|
|
|
|
import k1link.device_plugins.xgrids_k1.facade as facade_module
|
|
import k1link.host_network.wifi as host_wifi_module
|
|
from k1link.data_plane import ConsumerFrameContext, DecodedPointCloudView
|
|
from k1link.device_plugins.xgrids_k1.active_acquisition_recovery_checkpoint import (
|
|
ActiveAcquisitionRecoveryConnection,
|
|
ActiveAcquisitionRecoveryIdentity,
|
|
ActiveAcquisitionRecoveryTransportBinding,
|
|
active_acquisition_project_name_sha256,
|
|
)
|
|
from k1link.device_plugins.xgrids_k1.application_control_process_lease import (
|
|
ApplicationControlProcessLease,
|
|
)
|
|
from k1link.device_plugins.xgrids_k1.ble.runtime_arbiter import (
|
|
BleOperationHardTimeout,
|
|
BleOperationProgress,
|
|
BleRuntimeBusy,
|
|
bind_ble_runtime_owner_loop,
|
|
run_ble_operation,
|
|
wait_for_ble_runtime_idle,
|
|
)
|
|
from k1link.device_plugins.xgrids_k1.connection_supervisor import (
|
|
DEFAULT_TRANSPORT_OBSERVATION_TTL_SECONDS,
|
|
EndpointTarget,
|
|
HostPathProbeResult,
|
|
VerifiedControlEvidence,
|
|
)
|
|
from k1link.device_plugins.xgrids_k1.device_identity_pin_store import (
|
|
DeviceIdentityPinStoreCorrupt,
|
|
)
|
|
from k1link.device_plugins.xgrids_k1.facade import (
|
|
ACTION_PHYSICAL_COMMAND_REOPEN_RETIRED_RECONCILIATION,
|
|
ACTION_PHYSICAL_COMMAND_RETIRE_UNAVAILABLE,
|
|
DEFAULT_LIVE_STREAMS,
|
|
XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
AbortAcquisitionRequest,
|
|
BleScanRequest,
|
|
CameraPreviewSelectRequest,
|
|
CompatibilityAttestationRequest,
|
|
ConfiguredEndpointProbeRequest,
|
|
ConnectionVerifyRequest,
|
|
ConnectRequest,
|
|
DesiredConnectionModeRequest,
|
|
EnterApplicationWorkspaceRequest,
|
|
ForceFinishAcquisitionRequest,
|
|
OpenApplicationControlSessionRequest,
|
|
OperatorPresenceRequest,
|
|
PrepareAcquisitionRequest,
|
|
PrepareConnectionReconfigurationRequest,
|
|
ReopenRetiredPhysicalCommandReconciliationRequest,
|
|
RetireUnavailablePhysicalCommandRequest,
|
|
ShadowApplicationControlArmRequest,
|
|
StartAcquisitionRequest,
|
|
StopAcquisitionRequest,
|
|
XgridsK1CompatibilityService,
|
|
XgridsK1PluginFacade,
|
|
)
|
|
from k1link.device_plugins.xgrids_k1.network_mutation_ledger import (
|
|
NetworkConnectionMode,
|
|
NetworkMutationRecord,
|
|
NetworkStatusEvidence,
|
|
PreviousConnectionEvidence,
|
|
)
|
|
from k1link.device_plugins.xgrids_k1.network_provisioning_idempotency_journal import (
|
|
NetworkProvisioningIdempotencyConflict,
|
|
derive_request_binding_sha256,
|
|
)
|
|
from k1link.device_plugins.xgrids_k1.physical_command_coordinator import (
|
|
LedgerPhysicalCommandCoordinator,
|
|
PhysicalCommandIntentContext,
|
|
PhysicalCommandRuntimeBinding,
|
|
)
|
|
from k1link.device_plugins.xgrids_k1.physical_command_ledger import (
|
|
PHYSICAL_COMMAND_APPLICATION_SUCCESS_CODE,
|
|
PhysicalCommandApplicationResponse,
|
|
PhysicalCommandBlocked,
|
|
PhysicalCommandConnectionBinding,
|
|
PhysicalCommandIdentity,
|
|
PhysicalCommandLedgerSnapshot,
|
|
PhysicalCommandStatusEvidence,
|
|
PhysicalCommandTransitionError,
|
|
)
|
|
from k1link.device_plugins.xgrids_k1.protocol import application_session as session_module
|
|
from k1link.device_plugins.xgrids_k1.protocol.application_acceptance import (
|
|
ApplicationAcceptanceError,
|
|
)
|
|
from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
|
|
ApplicationControlAuthority,
|
|
LiveDeviceControlBinding,
|
|
)
|
|
from k1link.device_plugins.xgrids_k1.protocol.application_mqtt import (
|
|
ApplicationMqttDeviceStatusEvidence,
|
|
ApplicationMqttPublishEvidence,
|
|
ApplicationMqttResponseEvidence,
|
|
ApplicationMqttTransportError,
|
|
)
|
|
from k1link.device_plugins.xgrids_k1.protocol.application_publish import (
|
|
OneShotPublishEnvelope,
|
|
)
|
|
from k1link.device_plugins.xgrids_k1.protocol.application_session import (
|
|
ApplicationConnectionBinding,
|
|
ApplicationConnectionBindingLost,
|
|
InteractiveApplicationControlSession,
|
|
)
|
|
from k1link.device_plugins.xgrids_k1.protocol.modeling_control import (
|
|
MODELING_STATE_BASE,
|
|
)
|
|
from k1link.device_plugins.xgrids_k1.semantic_topology_store import (
|
|
SemanticTopologyStoreCorrupt,
|
|
)
|
|
from k1link.device_plugins.xgrids_k1.viewer.messages import StreamMessage
|
|
from k1link.host_network import HostWifiAssociationIdentityResult
|
|
from k1link.viewer.metrics import BridgeMetrics
|
|
from k1link.web.device_lifecycle import OperationJournal
|
|
from k1link.web.plugin_runtime import PluginExecutionError
|
|
|
|
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"
|
|
DURABLE_K1_UUID = "F89438FA-55ED-85AD-EED7-734AC84746D8"
|
|
_CONNECT_INTENT_SEQUENCE = count(1)
|
|
_LIFECYCLE_INTENT_SEQUENCE = count(1)
|
|
_SYNTHETIC_SCAN_CAPTURES: dict[str, facade_module.CapturedDiscoveredDevice] = {}
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _synthetic_exact_scan_handoff(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
|
|
"""Give facade-only lifecycle fixtures an exact selected scan object.
|
|
|
|
Scanner ownership/object handoff itself is covered in ``test_ble_scanner``.
|
|
These service tests seed the facade's scan projection directly, so mirror
|
|
that synthetic scan with one object-bound capture instead of weakening the
|
|
production admission rule to accept a UUID without a CoreBluetooth object.
|
|
"""
|
|
|
|
_SYNTHETIC_SCAN_CAPTURES.clear()
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_capture_network_intent_device",
|
|
lambda device_id: _SYNTHETIC_SCAN_CAPTURES.get(device_id),
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"pin_connected_device_handle",
|
|
lambda *_args, **_kwargs: None,
|
|
)
|
|
yield
|
|
_SYNTHETIC_SCAN_CAPTURES.clear()
|
|
|
|
|
|
def _connect_request(**values: Any) -> ConnectRequest:
|
|
"""Give every physical network test an explicit, unique durable intent."""
|
|
|
|
values.setdefault(
|
|
"idempotency_key",
|
|
f"test-network-provision-{next(_CONNECT_INTENT_SEQUENCE)}",
|
|
)
|
|
values.setdefault("expected_mode_revision", 0)
|
|
values.setdefault("expected_discovery_generation", 0)
|
|
return ConnectRequest(**values)
|
|
|
|
|
|
def _select_connection_mode(
|
|
service: XgridsK1CompatibilityService,
|
|
connection_mode: str,
|
|
) -> int:
|
|
"""Select a process-local draft exactly as the product dropdown does."""
|
|
|
|
state = service.select_connection_mode(
|
|
DesiredConnectionModeRequest(
|
|
connection_mode=connection_mode, # type: ignore[arg-type]
|
|
expected_revision=service.state()["desired_connection_mode_revision"],
|
|
)
|
|
)
|
|
return int(state["desired_connection_mode_revision"])
|
|
|
|
|
|
def _lifecycle_intent_key(action: str) -> str:
|
|
return f"test-{action}-{next(_LIFECYCLE_INTENT_SEQUENCE)}"
|
|
|
|
|
|
def _prepare_request(**values: Any) -> PrepareAcquisitionRequest:
|
|
values.setdefault("idempotency_key", _lifecycle_intent_key("acquisition-prepare"))
|
|
values.setdefault("expected_control_session_generation", 1)
|
|
values.setdefault("expected_control_state_revision", 1)
|
|
return PrepareAcquisitionRequest(**values)
|
|
|
|
|
|
def _start_request(**values: Any) -> StartAcquisitionRequest:
|
|
values.setdefault("idempotency_key", _lifecycle_intent_key("acquisition-start"))
|
|
values.setdefault("expected_control_session_generation", 1)
|
|
values.setdefault("expected_control_state_revision", 2)
|
|
return StartAcquisitionRequest(**values)
|
|
|
|
|
|
def _stop_request(**values: Any) -> StopAcquisitionRequest:
|
|
values.setdefault("idempotency_key", _lifecycle_intent_key("acquisition-stop"))
|
|
values.setdefault("expected_control_session_generation", 1)
|
|
values.setdefault("expected_control_state_revision", 3)
|
|
return StopAcquisitionRequest(**values)
|
|
|
|
|
|
def _abort_request(**values: Any) -> AbortAcquisitionRequest:
|
|
values.setdefault("idempotency_key", _lifecycle_intent_key("acquisition-abort"))
|
|
values.setdefault("expected_control_session_generation", 1)
|
|
values.setdefault("expected_control_state_revision", 2)
|
|
return AbortAcquisitionRequest(**values)
|
|
|
|
|
|
def _force_finish_request(**values: Any) -> ForceFinishAcquisitionRequest:
|
|
values.setdefault(
|
|
"idempotency_key",
|
|
_lifecycle_intent_key("acquisition-force-finish-local"),
|
|
)
|
|
values.setdefault("operator_confirmed", True)
|
|
return ForceFinishAcquisitionRequest(**values)
|
|
|
|
|
|
def _dispatch_test_network_write(
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None,
|
|
) -> None:
|
|
"""Cross the same durable dispatch boundary as the production BLE helper."""
|
|
|
|
assert on_write_dispatch is not None
|
|
on_write_dispatch(
|
|
{
|
|
"mode": "WIFI_AP",
|
|
"ipv4": "192.168.56.1",
|
|
"status_code": 1,
|
|
"reserved": 0,
|
|
},
|
|
"with_response",
|
|
)
|
|
|
|
|
|
async def _reachable_control_endpoint(
|
|
target: str,
|
|
*_: object,
|
|
endpoint_probe: Callable[[str], object] | None = None,
|
|
**__: object,
|
|
) -> object:
|
|
return endpoint_probe(target) if endpoint_probe is not None else True
|
|
|
|
|
|
async def _unreachable_control_endpoint(
|
|
target: str,
|
|
*_: object,
|
|
endpoint_probe: Callable[[str], object] | None = None,
|
|
**__: object,
|
|
) -> object:
|
|
return endpoint_probe(target) if endpoint_probe is not None else False
|
|
|
|
|
|
def _direct_host_path(target_ipv4: str, **_: object) -> HostPathProbeResult:
|
|
source_ipv4 = (
|
|
"10.255.254.2"
|
|
if target_ipv4.startswith("10.")
|
|
else "192.168.56.2"
|
|
if target_ipv4.startswith("192.168.56.")
|
|
else "192.168.1.2"
|
|
)
|
|
return HostPathProbeResult(
|
|
available=True,
|
|
fingerprint=f"test-route:{target_ipv4}",
|
|
interface="test0",
|
|
source_ipv4=source_ipv4,
|
|
route_class="direct",
|
|
kernel_route_fingerprint=f"test-route:{target_ipv4}",
|
|
)
|
|
|
|
|
|
def _association_bound_direct_host_path(target_ipv4: str) -> HostPathProbeResult:
|
|
raw = _direct_host_path(target_ipv4)
|
|
assert raw.fingerprint is not None
|
|
return HostPathProbeResult(
|
|
available=True,
|
|
fingerprint=facade_module.bind_route_fingerprint_to_wifi_association(
|
|
raw.fingerprint,
|
|
DeterministicContendedAssociationProbe.proven_observation(),
|
|
),
|
|
interface=raw.interface,
|
|
source_ipv4=raw.source_ipv4,
|
|
route_class=raw.route_class,
|
|
kernel_route_fingerprint=raw.fingerprint,
|
|
)
|
|
|
|
|
|
def _tunnel_host_path(target_ipv4: str) -> HostPathProbeResult:
|
|
return HostPathProbeResult(
|
|
available=True,
|
|
fingerprint=f"test-tunnel:{target_ipv4}",
|
|
interface="utun-test",
|
|
source_ipv4="100.64.0.2",
|
|
route_class="tunnel",
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("interface", "destination", "expected", "reason_code"),
|
|
[
|
|
("en0", "192.168.68.0", "direct", None),
|
|
("bridge100", "192.168.68.0", "direct", None),
|
|
("utun8", "192.168.68.0", "tunnel", "host-route-tunnel"),
|
|
("tun0", "192.168.68.0", "tunnel", "host-route-tunnel"),
|
|
("tap2", "192.168.68.0", "tunnel", "host-route-tunnel"),
|
|
("wg0", "192.168.68.0", "tunnel", "host-route-tunnel"),
|
|
("en0", "default", "default", "host-route-default"),
|
|
(
|
|
"vendor-vpn0",
|
|
"192.168.68.0",
|
|
"unknown",
|
|
"host-route-interface-unreviewed",
|
|
),
|
|
(None, "192.168.68.0", "unknown", "host-route-unclassified"),
|
|
],
|
|
)
|
|
def test_host_route_classifier_is_fail_closed_for_virtual_and_unknown_interfaces(
|
|
interface: str | None,
|
|
destination: str,
|
|
expected: str,
|
|
reason_code: str | None,
|
|
) -> None:
|
|
assert facade_module._classify_host_route(interface, destination) == ( # noqa: SLF001
|
|
expected,
|
|
reason_code,
|
|
)
|
|
|
|
|
|
class FakeHostWifiAssociationProbe:
|
|
def __init__(self, *continuity_tokens: str) -> None:
|
|
self._continuity_tokens = continuity_tokens or ("a" * 64,)
|
|
self.interfaces: list[str | None] = []
|
|
self.timeout_seconds: list[float] = []
|
|
|
|
def observe(
|
|
self,
|
|
interface_name: str | None,
|
|
*,
|
|
timeout_seconds: float = 30.0,
|
|
) -> HostWifiAssociationIdentityResult:
|
|
assert timeout_seconds > 0
|
|
self.timeout_seconds.append(timeout_seconds)
|
|
token_index = min(len(self.interfaces), len(self._continuity_tokens) - 1)
|
|
self.interfaces.append(interface_name)
|
|
return {
|
|
"schema_version": 1,
|
|
"adapter": "test-host-association",
|
|
"wifi_interface": True,
|
|
"association_state": "associated",
|
|
"evidence_quality": "bssid-only",
|
|
"continuity_proven": True,
|
|
"continuity_token": self._continuity_tokens[token_index],
|
|
"reason_code": None,
|
|
}
|
|
|
|
|
|
class DeterministicContendedAssociationProbe:
|
|
"""Real probe harness with fake lock/helper clocks and no wall-clock sleep."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
lock_wait_seconds: float,
|
|
helper_required_seconds: float,
|
|
on_first_lock_acquired: Callable[[], None] | None = None,
|
|
) -> None:
|
|
self.monotonic = 100.0
|
|
self.wall = 1_000.0
|
|
self.lock_wait_seconds = lock_wait_seconds
|
|
self.helper_required_seconds = helper_required_seconds
|
|
self.on_first_lock_acquired = on_first_lock_acquired
|
|
self.lock_timeouts: list[float] = []
|
|
self.helper_timeouts: list[float] = []
|
|
self._lock_acquisitions = 0
|
|
self._locked = False
|
|
|
|
def advance(self, seconds: float) -> None:
|
|
self.monotonic += seconds
|
|
self.wall += seconds
|
|
|
|
def acquire(self, *, timeout: float) -> bool:
|
|
self.lock_timeouts.append(timeout)
|
|
wait = self.lock_wait_seconds if self._lock_acquisitions == 0 else 0.0
|
|
self._lock_acquisitions += 1
|
|
if wait > timeout:
|
|
self.advance(timeout)
|
|
return False
|
|
self.advance(wait)
|
|
self._locked = True
|
|
if self._lock_acquisitions == 1 and self.on_first_lock_acquired is not None:
|
|
self.on_first_lock_acquired()
|
|
return True
|
|
|
|
def release(self) -> None:
|
|
assert self._locked is True
|
|
self._locked = False
|
|
|
|
def runner(
|
|
self,
|
|
argv: list[str],
|
|
**kwargs: object,
|
|
) -> subprocess.CompletedProcess[bytes]:
|
|
timeout = kwargs.get("timeout")
|
|
assert isinstance(timeout, (int, float)) and not isinstance(timeout, bool)
|
|
timeout_seconds = float(timeout)
|
|
self.helper_timeouts.append(timeout_seconds)
|
|
if self.helper_required_seconds > timeout_seconds:
|
|
self.advance(timeout_seconds)
|
|
raise subprocess.TimeoutExpired(argv, timeout_seconds)
|
|
self.advance(self.helper_required_seconds)
|
|
return subprocess.CompletedProcess(
|
|
argv,
|
|
0,
|
|
stdout=(
|
|
b'{"ok":true,"adapter":"CoreWLAN","wifi_interface":true,'
|
|
b'"association_identity":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
|
|
b'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","association_evidence":"bssid-only"}'
|
|
),
|
|
stderr=b"",
|
|
)
|
|
|
|
def build(
|
|
self,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> host_wifi_module.HostWifiAssociationIdentityProbe:
|
|
helper_path = tmp_path / "deterministic-association-helper.swift"
|
|
helper_path.write_text("// deterministic test helper\n", encoding="utf-8")
|
|
monkeypatch.setattr(
|
|
host_wifi_module,
|
|
"time",
|
|
SimpleNamespace(monotonic=lambda: self.monotonic),
|
|
)
|
|
monkeypatch.setattr(
|
|
host_wifi_module,
|
|
"sys",
|
|
SimpleNamespace(platform="darwin"),
|
|
)
|
|
probe = host_wifi_module.HostWifiAssociationIdentityProbe(
|
|
helper_path,
|
|
runner=self.runner,
|
|
continuity_key=b"c" * 32,
|
|
monotonic_clock=lambda: self.monotonic,
|
|
wall_clock=lambda: self.wall,
|
|
)
|
|
probe._lock = self # type: ignore[assignment] # noqa: SLF001
|
|
return probe
|
|
|
|
@staticmethod
|
|
def proven_observation() -> HostWifiAssociationIdentityResult:
|
|
return {
|
|
"schema_version": 1,
|
|
"adapter": "CoreWLAN",
|
|
"wifi_interface": True,
|
|
"association_state": "associated",
|
|
"evidence_quality": "bssid-only",
|
|
"continuity_proven": True,
|
|
"continuity_token": "a" * 64,
|
|
"reason_code": None,
|
|
}
|
|
|
|
|
|
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.producer_generation = 0
|
|
self.pcl_frames = 0
|
|
self.start_calls: list[tuple[str, Path, float | None, str]] = []
|
|
self.stop_calls = 0
|
|
self.stop_error: Exception | None = None
|
|
self.recover_connection: Callable[[int], str] | None = None
|
|
self.recovery_requests: list[tuple[str, int]] = []
|
|
self.recovery_request_pending = False
|
|
self.recovery_state = "inactive"
|
|
self.recovery_attempt = 0
|
|
|
|
def snapshot(self) -> dict[str, Any]:
|
|
return {
|
|
"phase": self.phase,
|
|
"message": "test runtime",
|
|
"source_mode": self.source_mode,
|
|
"source_ready": self.source_ready,
|
|
"producer_generation": self.producer_generation,
|
|
"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,
|
|
},
|
|
"connection_recovery": {
|
|
"state": self.recovery_state,
|
|
"attempt": self.recovery_attempt,
|
|
"reason_code": None,
|
|
"started_at_utc": None,
|
|
"elapsed_ms": None,
|
|
"recovered_at_utc": None,
|
|
"automatic_command_retry": False,
|
|
"device_write_performed": False,
|
|
"network_mutation_performed": False,
|
|
},
|
|
}
|
|
|
|
def start_live(
|
|
self,
|
|
host: str,
|
|
out_dir: Path,
|
|
*,
|
|
duration_seconds: float | None,
|
|
project_name: str,
|
|
recover_connection: Callable[[int], str] | None = None,
|
|
) -> None:
|
|
self.producer_generation += 1
|
|
self.start_calls.append((host, out_dir, duration_seconds, project_name))
|
|
self.phase = "starting_live"
|
|
self.source_mode = "live"
|
|
self.recover_connection = recover_connection
|
|
|
|
def mark_ready(self) -> None:
|
|
self.phase = "live"
|
|
self.source_ready = True
|
|
|
|
def request_connection_recovery(
|
|
self,
|
|
reason_code: str,
|
|
*,
|
|
expected_generation: int,
|
|
) -> bool:
|
|
if (
|
|
expected_generation != self.producer_generation
|
|
or self.recover_connection is None
|
|
or self.source_mode != "live"
|
|
or self.phase not in {"live", "reconnecting"}
|
|
):
|
|
return False
|
|
if self.phase == "reconnecting":
|
|
return True
|
|
self.phase = "reconnecting"
|
|
self.source_ready = False
|
|
self.recovery_state = "reconnecting"
|
|
self.recovery_requests.append((reason_code, expected_generation))
|
|
self.recovery_request_pending = True
|
|
return True
|
|
|
|
def stop(self) -> None:
|
|
self.stop_calls += 1
|
|
self.recovery_request_pending = False
|
|
if self.stop_error is not None:
|
|
raise self.stop_error
|
|
self.phase = "idle"
|
|
self.source_mode = "idle"
|
|
self.source_ready = False
|
|
self.recovery_state = "inactive"
|
|
self.recovery_attempt = 0
|
|
|
|
def close(self) -> None:
|
|
self.stop()
|
|
|
|
|
|
class FakeInteractiveControlSession:
|
|
def __init__(self, *, initial_state: str = "workspace-ready") -> None:
|
|
self.state = initial_state
|
|
self.session_generation = 1
|
|
self.state_revision = 1
|
|
self.verified_control: dict[str, object] | None = None
|
|
self.start_projects: list[str] = []
|
|
self.start_contexts: list[Any] = []
|
|
self.stop_calls = 0
|
|
self.stop_contexts: list[Any] = []
|
|
self.stop_dispatch_deadlines: list[Callable[[], bool] | None] = []
|
|
self.inspection_only = False
|
|
self.inspection_promotion_allowed = False
|
|
self.failure: dict[str, object] | None = None
|
|
self.outcome_unknown = False
|
|
self.transport_publish_attempts = 0
|
|
|
|
def snapshot(self) -> dict[str, object]:
|
|
return {
|
|
"state": self.state,
|
|
"session_generation": self.session_generation,
|
|
"state_revision": self.state_revision,
|
|
"inspection_only": self.inspection_only,
|
|
"inspection_promotion_allowed": self.inspection_promotion_allowed,
|
|
"can_open": self.state in {"idle", "completed", "closed", "failed"},
|
|
"can_enter_workspace": self.state == "connection-ready",
|
|
"can_prepare_project": self.state == "workspace-ready",
|
|
"can_start": self.state == "project-ready",
|
|
"can_stop": self.state == "scanning",
|
|
"can_confirm_standby": False,
|
|
"failure": self.failure,
|
|
"outcome_unknown": self.outcome_unknown,
|
|
"transport": {
|
|
"state": "failed" if self.state == "failed" else "ready",
|
|
"publish_attempts": self.transport_publish_attempts,
|
|
},
|
|
"verified_control": self.verified_control,
|
|
}
|
|
|
|
def open(
|
|
self,
|
|
*,
|
|
connection_binding: ApplicationConnectionBinding,
|
|
inspection_only: bool = False,
|
|
**_: object,
|
|
) -> dict[str, object]:
|
|
assert self.state == "idle"
|
|
self.verified_control = _verified_control_for_binding(connection_binding)
|
|
self.inspection_only = inspection_only
|
|
self.inspection_promotion_allowed = not inspection_only
|
|
self.state = "connection-ready"
|
|
self.state_revision += 1
|
|
return self.snapshot()
|
|
|
|
def adopt_reconciled_scanning(
|
|
self,
|
|
*,
|
|
reconciliation_id: str,
|
|
expected_session_generation: int,
|
|
expected_state_revision: int,
|
|
) -> dict[str, object]:
|
|
assert reconciliation_id
|
|
self._accept_checkpoint(
|
|
expected_session_generation=expected_session_generation,
|
|
expected_state_revision=expected_state_revision,
|
|
)
|
|
assert self.state == "connection-ready"
|
|
self.state = "scanning"
|
|
return self.snapshot()
|
|
|
|
def enter_workspace(
|
|
self,
|
|
*,
|
|
expected_session_generation: int | None = None,
|
|
expected_state_revision: int | None = None,
|
|
) -> dict[str, object]:
|
|
assert self.state == "connection-ready"
|
|
self._accept_checkpoint(
|
|
expected_session_generation=expected_session_generation,
|
|
expected_state_revision=expected_state_revision,
|
|
)
|
|
self.state = "workspace-ready"
|
|
return self.snapshot()
|
|
|
|
def _accept_checkpoint(
|
|
self,
|
|
*,
|
|
expected_session_generation: int | None,
|
|
expected_state_revision: int | None,
|
|
) -> None:
|
|
assert expected_session_generation == self.session_generation
|
|
assert expected_state_revision == self.state_revision
|
|
self.state_revision += 1
|
|
|
|
def validate_connection_binding(self) -> None:
|
|
return None
|
|
|
|
def validate_physical_reconciliation_binding(self) -> None:
|
|
return None
|
|
|
|
def open_project_prompt(
|
|
self,
|
|
*,
|
|
expected_session_generation: int | None = None,
|
|
expected_state_revision: int | None = None,
|
|
) -> dict[str, object]:
|
|
assert self.state == "workspace-ready"
|
|
self._accept_checkpoint(
|
|
expected_session_generation=expected_session_generation,
|
|
expected_state_revision=expected_state_revision,
|
|
)
|
|
self.state = "project-ready"
|
|
return self.snapshot()
|
|
|
|
def request_start(
|
|
self,
|
|
*,
|
|
project_name: str,
|
|
confirmation: object,
|
|
command_context: object,
|
|
preparation_checkpoint_observer: object | None = None,
|
|
expected_session_generation: int | None = None,
|
|
expected_state_revision: int | None = None,
|
|
) -> dict[str, object]:
|
|
del preparation_checkpoint_observer
|
|
assert confirmation is not None
|
|
assert command_context is not None
|
|
assert self.state == "project-ready"
|
|
self._accept_checkpoint(
|
|
expected_session_generation=expected_session_generation,
|
|
expected_state_revision=expected_state_revision,
|
|
)
|
|
self.start_projects.append(project_name)
|
|
self.start_contexts.append(command_context)
|
|
self.state = "scanning"
|
|
return self.snapshot()
|
|
|
|
def request_stop(
|
|
self,
|
|
*,
|
|
confirmation: object,
|
|
command_context: object,
|
|
dispatch_admission_deadline_reached: Callable[[], bool] | None = None,
|
|
expected_session_generation: int | None = None,
|
|
expected_state_revision: int | None = None,
|
|
) -> dict[str, object]:
|
|
assert confirmation is not None
|
|
assert command_context is not None
|
|
assert self.state == "scanning"
|
|
self._accept_checkpoint(
|
|
expected_session_generation=expected_session_generation,
|
|
expected_state_revision=expected_state_revision,
|
|
)
|
|
self.stop_calls += 1
|
|
self.stop_contexts.append(command_context)
|
|
self.stop_dispatch_deadlines.append(dispatch_admission_deadline_reached)
|
|
self.state = "awaiting-standby-confirmation"
|
|
return self.snapshot()
|
|
|
|
def close_prestart(
|
|
self,
|
|
*,
|
|
expected_session_generation: int | None = None,
|
|
expected_state_revision: int | None = None,
|
|
) -> dict[str, object]:
|
|
if expected_session_generation is not None or expected_state_revision is not None:
|
|
self._accept_checkpoint(
|
|
expected_session_generation=expected_session_generation,
|
|
expected_state_revision=expected_state_revision,
|
|
)
|
|
self.state = "closed"
|
|
self.verified_control = None
|
|
return self.snapshot()
|
|
|
|
def retire_for_network_change(self, **_: object) -> dict[str, object]:
|
|
if self.state not in {"idle", "completed", "closed", "failed"}:
|
|
raise RuntimeError("control session cannot be retired")
|
|
self.state = "idle"
|
|
self.verified_control = None
|
|
return self.snapshot()
|
|
|
|
def close(self) -> None:
|
|
self.state = "closed"
|
|
self.verified_control = None
|
|
|
|
|
|
PHYSICAL_ACCEPTANCE = OperatorPresenceRequest(
|
|
operator_present=True,
|
|
owner_controlled_device=True,
|
|
lixelgo_closed=True,
|
|
battery_storage_confirmed=True,
|
|
expected_physical_state_confirmed=True,
|
|
)
|
|
|
|
|
|
def _exact_start_physical_proof(
|
|
*,
|
|
operation_id: str,
|
|
acquisition_id: str,
|
|
binding: ApplicationConnectionBinding,
|
|
resolved: bool,
|
|
) -> dict[str, object]:
|
|
session_state = "scanning" if resolved else "scan_starting"
|
|
return {
|
|
"status": "resolved" if resolved else "unresolved",
|
|
"reason_code": (None if resolved else "physical-command-reconciliation-required"),
|
|
"requires_reconciliation": not resolved,
|
|
"resolved_active_recovery_required": resolved,
|
|
"reconciled_physical_state": "active" if resolved else None,
|
|
"physical_active": resolved,
|
|
"automatic_replay_allowed": False,
|
|
"record": {
|
|
"revision": 7,
|
|
"operation_id": operation_id,
|
|
"acquisition_id": acquisition_id,
|
|
"action": "start",
|
|
"stage": "resolved" if resolved else "observing",
|
|
"resolution": "start-active-observed" if resolved else None,
|
|
"publish_call_returned": True,
|
|
"qos2_completed": True,
|
|
"packet_id": 14,
|
|
"application_response": {
|
|
"operation_id": operation_id,
|
|
"action": "start",
|
|
"success": True,
|
|
},
|
|
"baseline_status": {
|
|
"control_session_id": "fake-control-session",
|
|
"host_path_epoch": binding.host_path_epoch,
|
|
"producer_generation": 1,
|
|
"session_state": "ready",
|
|
"project_bound": False,
|
|
"init_ready": False,
|
|
"mqtt_retained": False,
|
|
},
|
|
"last_status": {
|
|
"session_state": session_state,
|
|
"project_bound": resolved,
|
|
"init_ready": resolved,
|
|
"mqtt_retained": False,
|
|
"system_error_code": None,
|
|
},
|
|
"connection": {
|
|
"intent_id": binding.intent_id,
|
|
"transport_ref": binding.transport_ref,
|
|
"connection_mode": binding.connection_mode,
|
|
"target_ipv4": binding.target_ipv4,
|
|
"target_port": binding.target_port,
|
|
"host_path_epoch": binding.host_path_epoch,
|
|
"control_session_id": "fake-control-session",
|
|
"producer_generation": 1,
|
|
},
|
|
"identity": {
|
|
"vendor_device_id_sha256": "a" * 64,
|
|
"device_serial_sha256": "b" * 64,
|
|
},
|
|
"compatibility_profile_id": XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
"reconciliations": [],
|
|
},
|
|
}
|
|
|
|
|
|
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()
|
|
|
|
|
|
async def _synthetic_prestart_control_bootstrap(
|
|
service: XgridsK1CompatibilityService,
|
|
*,
|
|
parent_operation_id: str | None,
|
|
connection_mode: facade_module.ConnectionMode,
|
|
inspection_only: bool = False,
|
|
) -> None:
|
|
"""Keep network lifecycle tests independent from a live MQTT/Keychain."""
|
|
|
|
supervisor = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
assert supervisor.intent is not None
|
|
assert supervisor.device_network.target is not None
|
|
assert supervisor.device_network.transport_ref is not None
|
|
operation, _ = service._operations.begin( # noqa: SLF001
|
|
facade_module.ACTION_CONNECTION_CONTROL_BOOTSTRAP,
|
|
device_id=service._device_id, # noqa: SLF001
|
|
device_session_id=service._device_session_id, # noqa: SLF001
|
|
deadline_seconds=1.0,
|
|
context={
|
|
"connection_mode": connection_mode,
|
|
"parent_operation_id": parent_operation_id,
|
|
"automatic_retry": False,
|
|
},
|
|
)
|
|
service._operations.transition( # noqa: SLF001
|
|
operation.operation_id,
|
|
"running",
|
|
stage_code="mqtt-device-info",
|
|
message_code="connection.control_bootstrap.running",
|
|
)
|
|
control = FakeInteractiveControlSession(initial_state="idle")
|
|
binding = ApplicationConnectionBinding(
|
|
intent_id=supervisor.intent.intent_id,
|
|
transport_ref=supervisor.device_network.transport_ref,
|
|
host_path_epoch=supervisor.host_path.epoch,
|
|
target_ipv4=supervisor.device_network.target.ipv4,
|
|
target_port=supervisor.device_network.target.port,
|
|
connection_mode=connection_mode,
|
|
)
|
|
control.open(
|
|
connection_binding=binding,
|
|
inspection_only=inspection_only,
|
|
)
|
|
control.verified_control = _verified_control_for_binding(
|
|
binding,
|
|
logical_device_id=str(
|
|
supervisor.intent.expected_device_id or service._device_id # noqa: SLF001
|
|
),
|
|
control_session_id=(
|
|
f"fake-control-{parent_operation_id}"
|
|
if parent_operation_id is not None
|
|
else "fake-control-session"
|
|
),
|
|
)
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
service._reconcile_connection_supervisor( # noqa: SLF001
|
|
control.snapshot(),
|
|
service.runtime.snapshot(),
|
|
)
|
|
if not service._connection_supervisor.snapshot().authority.control_allowed: # noqa: SLF001
|
|
failure = facade_module.ConnectionVerificationError(
|
|
"synthetic DeviceInfo authority was not admitted",
|
|
reason_code="control-bootstrap-device-identity-unverified",
|
|
)
|
|
service._operations.transition( # noqa: SLF001
|
|
operation.operation_id,
|
|
"failed",
|
|
stage_code="device-info-failed",
|
|
message_code="connection.control_bootstrap.failed",
|
|
error={
|
|
"category": "connection",
|
|
"code": failure.reason_code,
|
|
"retryable": True,
|
|
"safe_to_retry": True,
|
|
"side_effect_status": "none",
|
|
},
|
|
)
|
|
raise failure
|
|
service._operations.transition( # noqa: SLF001
|
|
operation.operation_id,
|
|
"succeeded",
|
|
stage_code="device-info-confirmed",
|
|
message_code="connection.control_bootstrap.completed",
|
|
result={
|
|
"connection_mode": connection_mode,
|
|
"control_verified": True,
|
|
"device_write_performed": False,
|
|
"automatic_retry": False,
|
|
},
|
|
)
|
|
|
|
|
|
def service_with_fake_runtime(
|
|
tmp_path: Path,
|
|
) -> tuple[XgridsK1CompatibilityService, FakeVisualizationRuntime]:
|
|
# Durable network/idempotency records are intentionally global in the real
|
|
# runtime. Each test needs its own private store while repeated service
|
|
# construction inside one test must still exercise restart semantics.
|
|
os.environ["MISSIONCORE_DATA_DIR"] = str(tmp_path / "private-data")
|
|
os.environ["MISSIONCORE_EVIDENCE_DIR"] = str(tmp_path / "private-evidence")
|
|
os.environ["MISSIONCORE_LEGACY_SESSIONS_DIR"] = str(tmp_path / "private-legacy-sessions")
|
|
service = XgridsK1CompatibilityService(
|
|
tmp_path,
|
|
host_wifi_association_probe=FakeHostWifiAssociationProbe(),
|
|
)
|
|
runtime = FakeVisualizationRuntime()
|
|
service.runtime = runtime # type: ignore[assignment]
|
|
# The broad legacy lifecycle fixture replaces both the real application
|
|
# session and typed physical coordinator with mapping-only fakes. It has no
|
|
# durable PREPARED callback from which checkpoint v2 can be constructed.
|
|
# Keep those older tests scoped to their original lifecycle concern; the
|
|
# typed checkpoint ordering/CAS seams have dedicated integration tests.
|
|
service._active_acquisition_checkpoint_matches_current = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
lambda _service, **_kwargs: True,
|
|
service,
|
|
)
|
|
legacy_start_operation_id: list[str | None] = [None]
|
|
|
|
def checkpoint_start_operation_id(
|
|
_service: XgridsK1CompatibilityService,
|
|
*,
|
|
local_start_operation_id: str | None,
|
|
**_kwargs: object,
|
|
) -> str | None:
|
|
if local_start_operation_id is not None:
|
|
legacy_start_operation_id[0] = local_start_operation_id
|
|
return legacy_start_operation_id[0]
|
|
|
|
service._active_acquisition_checkpoint_start_operation_id = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
checkpoint_start_operation_id,
|
|
service,
|
|
)
|
|
service._record_active_acquisition_checkpoint_gap = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
lambda _service, _lineage, **_kwargs: True,
|
|
service,
|
|
)
|
|
service._confirm_active_acquisition_checkpoint_rebind = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
lambda _service, *_args, **_kwargs: True,
|
|
service,
|
|
)
|
|
service._cease_active_acquisition_checkpoint_from_physical_head = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
lambda _service, **_kwargs: True,
|
|
service,
|
|
)
|
|
service._bootstrap_prestart_control_ready_owned = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
_synthetic_prestart_control_bootstrap,
|
|
service,
|
|
)
|
|
return service, runtime
|
|
|
|
|
|
def test_service_owns_one_wifi_association_probe_across_monitor_recreation(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
association_probe = FakeHostWifiAssociationProbe()
|
|
constructed: list[Path] = []
|
|
|
|
def probe_factory(helper_path: Path) -> FakeHostWifiAssociationProbe:
|
|
constructed.append(helper_path)
|
|
return association_probe
|
|
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"HostWifiAssociationIdentityProbe",
|
|
probe_factory,
|
|
)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
service = XgridsK1CompatibilityService(tmp_path)
|
|
_seed_supervised_connection(service, target_ipv4="192.168.1.20")
|
|
|
|
first = service._sample_host_path("192.168.1.20") # noqa: SLF001
|
|
service._connection_monitor = service._new_connection_monitor() # noqa: SLF001
|
|
second = asyncio.run( # noqa: SLF001
|
|
service._monitor_host_path(EndpointTarget("192.168.1.20"))
|
|
)
|
|
|
|
assert constructed == [tmp_path / "plugins" / "xgrids-k1" / "macos" / "associate_wifi.swift"]
|
|
assert association_probe.interfaces == ["test0", "test0"]
|
|
assert first.fingerprint == second.fingerprint
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("raw_paths", "association_tokens"),
|
|
[
|
|
(
|
|
(
|
|
_direct_host_path("192.168.1.20"),
|
|
_direct_host_path("192.168.56.20"),
|
|
),
|
|
("a" * 64, "a" * 64, "a" * 64),
|
|
),
|
|
(
|
|
(
|
|
_direct_host_path("192.168.1.20"),
|
|
_direct_host_path("192.168.1.20"),
|
|
),
|
|
("a" * 64, "a" * 64, "b" * 64),
|
|
),
|
|
],
|
|
)
|
|
def test_sync_tcp_success_cannot_cross_route_or_wifi_association_change(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
raw_paths: tuple[HostPathProbeResult, HostPathProbeResult],
|
|
association_tokens: tuple[str, str, str],
|
|
) -> None:
|
|
association_probe = FakeHostWifiAssociationProbe(*association_tokens)
|
|
service = XgridsK1CompatibilityService(
|
|
tmp_path,
|
|
host_wifi_association_probe=association_probe,
|
|
)
|
|
service.runtime = FakeVisualizationRuntime() # type: ignore[assignment]
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", lambda _target: raw_paths[0])
|
|
initial_path = service._sample_host_path("192.168.1.20") # noqa: SLF001
|
|
_seed_supervised_connection(service, host_path=initial_path)
|
|
pending_paths = iter(raw_paths)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_inspect_host_path",
|
|
lambda _target: next(pending_paths),
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_control_endpoint_reachable",
|
|
lambda _target: True,
|
|
)
|
|
|
|
observation = service._probe_control_endpoint("192.168.1.20") # noqa: SLF001
|
|
supervisor = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
|
|
assert observation.reachable is False
|
|
assert observation.reason_code == "host-path-changed-during-tcp-probe"
|
|
assert supervisor.host_path.fingerprint == observation.path.fingerprint
|
|
assert supervisor.endpoint.tcp_state == "unreachable"
|
|
assert supervisor.authority.control_allowed is False
|
|
assert supervisor.lease.state != "reachable"
|
|
|
|
|
|
def _seed_supervised_connection(
|
|
service: XgridsK1CompatibilityService,
|
|
*,
|
|
target_ipv4: str = "192.168.1.20",
|
|
connection_mode: facade_module.ConnectionMode = "bridge",
|
|
transport_ref: str = "test-ble-transport",
|
|
logical_device_id: str = "known-k1",
|
|
host_path: HostPathProbeResult | None = None,
|
|
endpoint_reachable: bool = True,
|
|
endpoint_reason: str | None = None,
|
|
with_control: bool = True,
|
|
) -> ApplicationConnectionBinding:
|
|
"""Install one internally consistent test-only connection evidence chain.
|
|
|
|
Older lifecycle fixtures used ``_k1_ip`` as if an address implied a live
|
|
connection. The production supervisor now deliberately requires four
|
|
independent facts: the K1-reported topology, the current host route, TCP
|
|
reachability and DeviceInfo bound to that exact intent/route epoch. Tests
|
|
that exercise already-open plugin control must seed all four explicitly;
|
|
tests for the pre-DeviceInfo state pass ``with_control=False``.
|
|
"""
|
|
|
|
intent_id = f"test-intent-{connection_mode}-{transport_ref}-{target_ipv4}"
|
|
target = EndpointTarget(target_ipv4, facade_module.CONTROL_MQTT_PORT)
|
|
attestation = {
|
|
"bridge": ATTESTATION,
|
|
"quick-connect": QUICK_CONNECT_ATTESTATION,
|
|
"direct-connect": DIRECT_CONNECT_ATTESTATION,
|
|
}[connection_mode]
|
|
service._pin_or_match_device_identity( # noqa: SLF001
|
|
transport_ref=transport_ref,
|
|
logical_device_id=logical_device_id,
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
)
|
|
service._connection_supervisor.set_intent( # noqa: SLF001
|
|
intent_id=intent_id,
|
|
requested_mode=connection_mode,
|
|
expected_device_id=logical_device_id,
|
|
)
|
|
accepted = service._connection_supervisor.observe_device_network_applied( # noqa: SLF001
|
|
intent_id=intent_id,
|
|
transport_ref=transport_ref,
|
|
connection_mode=connection_mode,
|
|
target=target,
|
|
source="ble-read-only-status",
|
|
)
|
|
assert accepted is True
|
|
host_epoch = service._connection_supervisor.observe_host_path( # noqa: SLF001
|
|
host_path or _direct_host_path(target_ipv4)
|
|
)
|
|
endpoint_accepted = service._connection_supervisor.observe_endpoint( # noqa: SLF001
|
|
target=target,
|
|
intent_id=intent_id,
|
|
host_path_epoch=host_epoch,
|
|
reachable=endpoint_reachable,
|
|
reason_code=(
|
|
None if endpoint_reachable else endpoint_reason or "test-endpoint-unreachable"
|
|
),
|
|
)
|
|
assert endpoint_accepted is True
|
|
binding = ApplicationConnectionBinding(
|
|
intent_id=intent_id,
|
|
transport_ref=transport_ref,
|
|
host_path_epoch=host_epoch,
|
|
target_ipv4=target_ipv4,
|
|
target_port=facade_module.CONTROL_MQTT_PORT,
|
|
connection_mode=connection_mode,
|
|
)
|
|
if with_control:
|
|
assert endpoint_reachable is True
|
|
control_accepted = service._connection_supervisor.observe_control_evidence( # noqa: SLF001
|
|
VerifiedControlEvidence(
|
|
intent_id=intent_id,
|
|
transport_ref=transport_ref,
|
|
host_path_epoch=host_epoch,
|
|
target=target,
|
|
connection_mode=connection_mode,
|
|
logical_device_id=logical_device_id,
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
control_session_id=f"test-control-{transport_ref}",
|
|
)
|
|
)
|
|
assert control_accepted is True
|
|
|
|
with service._lock: # noqa: SLF001
|
|
service._selected_device_id = transport_ref # noqa: SLF001
|
|
service._connection_mode = connection_mode # noqa: SLF001
|
|
service._k1_ip = target_ipv4 # noqa: SLF001
|
|
service._device_id = logical_device_id # noqa: SLF001
|
|
service._device_session_id = f"test-session-{transport_ref}" # noqa: SLF001
|
|
service._device_session_opened_at = "2026-07-20T12:00:00Z" # noqa: SLF001
|
|
service._compatibility_attestation = attestation.model_dump(mode="json") # noqa: SLF001
|
|
service._connection_lease_generation = ( # noqa: SLF001
|
|
service._connection_supervisor.snapshot().lease.generation # noqa: SLF001
|
|
)
|
|
control = service._application_control_session # noqa: SLF001
|
|
if (
|
|
with_control
|
|
and isinstance(control, FakeInteractiveControlSession)
|
|
and control.verified_control is None
|
|
):
|
|
control.verified_control = _verified_control_for_binding(
|
|
binding,
|
|
logical_device_id=logical_device_id,
|
|
)
|
|
return binding
|
|
|
|
|
|
def _verified_control_for_binding(
|
|
binding: ApplicationConnectionBinding,
|
|
*,
|
|
logical_device_id: str = "known-k1",
|
|
control_session_id: str = "fake-control-session",
|
|
control_proof_revision: int = 1,
|
|
control_proof_source: str = "correlated-application-response",
|
|
control_proof_fresh: bool = True,
|
|
producer_generation: int = 1,
|
|
) -> dict[str, object]:
|
|
return {
|
|
"logical_device_id": logical_device_id,
|
|
"compatibility_profile_id": XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
"control_session_id": control_session_id,
|
|
"source": "mqtt-device-info",
|
|
"intent_id": binding.intent_id,
|
|
"transport_ref": binding.transport_ref,
|
|
"host_path_epoch": binding.host_path_epoch,
|
|
"target_ipv4": binding.target_ipv4,
|
|
"target_port": binding.target_port,
|
|
"connection_mode": binding.connection_mode,
|
|
"producer_generation": producer_generation,
|
|
"control_proof_revision": control_proof_revision,
|
|
"control_proof_source": control_proof_source,
|
|
"control_proof_fresh": control_proof_fresh,
|
|
}
|
|
|
|
|
|
def _seed_unresolved_physical_stop_for_retirement(
|
|
service: XgridsK1CompatibilityService,
|
|
binding: ApplicationConnectionBinding,
|
|
) -> RetireUnavailablePhysicalCommandRequest:
|
|
"""Persist one accepted STOP whose READY outcome was never observed."""
|
|
|
|
identity = PhysicalCommandIdentity(
|
|
vendor_device_id_sha256=hashlib.sha256(b"retirement-vendor").hexdigest(),
|
|
device_serial_sha256=hashlib.sha256(b"retirement-serial").hexdigest(),
|
|
)
|
|
connection = PhysicalCommandConnectionBinding(
|
|
intent_id=binding.intent_id,
|
|
transport_ref=binding.transport_ref,
|
|
connection_mode=binding.connection_mode,
|
|
target_ipv4=binding.target_ipv4,
|
|
target_port=binding.target_port,
|
|
host_path_epoch=binding.host_path_epoch,
|
|
control_session_id="retirement-old-control-session",
|
|
producer_generation=11,
|
|
)
|
|
|
|
def status(
|
|
session_state: str,
|
|
*,
|
|
observed_at_utc: str,
|
|
) -> PhysicalCommandStatusEvidence:
|
|
scanning = session_state == "scanning"
|
|
return PhysicalCommandStatusEvidence(
|
|
source="live-control-session",
|
|
vendor_device_id_sha256=identity.vendor_device_id_sha256,
|
|
device_serial_sha256=identity.device_serial_sha256,
|
|
control_session_id=connection.control_session_id,
|
|
host_path_epoch=connection.host_path_epoch,
|
|
producer_generation=connection.producer_generation,
|
|
session_state=session_state, # type: ignore[arg-type]
|
|
session_state_code=302 if scanning else 300,
|
|
project_bound=scanning,
|
|
project_id_sha256=("c" * 64 if scanning else None),
|
|
init_ready=scanning,
|
|
status_message_sha256=hashlib.sha256(
|
|
f"{session_state}:{observed_at_utc}".encode()
|
|
).hexdigest(),
|
|
mqtt_retained=False,
|
|
observed_at_utc=observed_at_utc,
|
|
)
|
|
|
|
ledger = service._physical_command_ledger # noqa: SLF001
|
|
start_operation_id = "physical-start-before-operator-retirement"
|
|
stop_operation_id = "physical-stop-before-operator-retirement"
|
|
acquisition_id = "acquisition-before-operator-retirement"
|
|
ledger.prepare(
|
|
operation_id=start_operation_id,
|
|
parent_operation_id=None,
|
|
acquisition_id=acquisition_id,
|
|
action="start",
|
|
identity=identity,
|
|
connection=connection,
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
payload_sha256="d" * 64,
|
|
baseline_status=status(
|
|
"ready",
|
|
observed_at_utc="2026-08-10T16:59:00.000Z",
|
|
),
|
|
)
|
|
ledger.mark_dispatching(start_operation_id)
|
|
ledger.mark_observing(
|
|
start_operation_id,
|
|
publish_call_returned=True,
|
|
packet_id=41,
|
|
)
|
|
ledger.mark_qos2_completed(start_operation_id, packet_id=41)
|
|
ledger.record_application_response(
|
|
start_operation_id,
|
|
PhysicalCommandApplicationResponse(
|
|
operation_id=start_operation_id,
|
|
action="start",
|
|
control_session_id=connection.control_session_id,
|
|
host_path_epoch=connection.host_path_epoch,
|
|
producer_generation=connection.producer_generation,
|
|
result_code=PHYSICAL_COMMAND_APPLICATION_SUCCESS_CODE,
|
|
success=True,
|
|
payload_sha256="e" * 64,
|
|
observed_at_utc="2026-08-10T16:59:01.000Z",
|
|
),
|
|
)
|
|
ledger.record_status_observation(
|
|
start_operation_id,
|
|
status("scanning", observed_at_utc="2026-08-10T16:59:02.000Z"),
|
|
)
|
|
ledger.resolve(start_operation_id, resolution="start-active-observed")
|
|
ledger.prepare(
|
|
operation_id=stop_operation_id,
|
|
parent_operation_id=start_operation_id,
|
|
acquisition_id=acquisition_id,
|
|
action="stop",
|
|
identity=identity,
|
|
connection=connection,
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
payload_sha256="f" * 64,
|
|
baseline_status=status(
|
|
"scanning",
|
|
observed_at_utc="2026-08-10T17:00:00.000Z",
|
|
),
|
|
)
|
|
ledger.mark_dispatching(stop_operation_id)
|
|
ledger.mark_observing(
|
|
stop_operation_id,
|
|
publish_call_returned=True,
|
|
packet_id=42,
|
|
)
|
|
ledger.mark_qos2_completed(stop_operation_id, packet_id=42)
|
|
ledger.record_application_response(
|
|
stop_operation_id,
|
|
PhysicalCommandApplicationResponse(
|
|
operation_id=stop_operation_id,
|
|
action="stop",
|
|
control_session_id=connection.control_session_id,
|
|
host_path_epoch=connection.host_path_epoch,
|
|
producer_generation=connection.producer_generation,
|
|
result_code=PHYSICAL_COMMAND_APPLICATION_SUCCESS_CODE,
|
|
success=True,
|
|
payload_sha256="1" * 64,
|
|
observed_at_utc="2026-08-10T17:00:01.000Z",
|
|
),
|
|
)
|
|
record = ledger.snapshot().record
|
|
assert record is not None and record.unresolved
|
|
return RetireUnavailablePhysicalCommandRequest(
|
|
retirement_id="operator-retirement-stable-request",
|
|
expected_operation_id=record.operation_id,
|
|
expected_revision=record.revision,
|
|
expected_transport_ref=record.connection.transport_ref,
|
|
operator_confirmed=True,
|
|
reason="device-permanently-unavailable-or-replaced",
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"failure_mode",
|
|
["stale-visible-checkpoint", "cross-process-ledger-race"],
|
|
)
|
|
def test_physical_retirement_checkpoint_conflict_is_http_409_without_device_io(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
failure_mode: str,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(
|
|
service,
|
|
transport_ref=DURABLE_K1_UUID,
|
|
connection_mode="bridge",
|
|
)
|
|
current_request = _seed_unresolved_physical_stop_for_retirement(
|
|
service,
|
|
binding,
|
|
)
|
|
request = (
|
|
current_request.model_copy(
|
|
update={
|
|
"expected_operation_id": "physical-stop-different-current-checkpoint",
|
|
}
|
|
)
|
|
if failure_mode == "stale-visible-checkpoint"
|
|
else current_request
|
|
)
|
|
before = service.state()
|
|
before_record = service._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
before_operations = service._operations.snapshot() # noqa: SLF001
|
|
io_calls: list[str] = []
|
|
ledger_calls: list[str] = []
|
|
|
|
async def forbidden_async_io(*_args: object, **_kwargs: object) -> object:
|
|
io_calls.append("ble-or-wifi")
|
|
raise AssertionError("retirement checkpoint conflict must not reach device I/O")
|
|
|
|
def forbidden_sync_io(*_args: object, **_kwargs: object) -> object:
|
|
io_calls.append("mqtt-tcp-monitor")
|
|
raise AssertionError("retirement checkpoint conflict must not probe the device")
|
|
|
|
def final_ledger_cas(*_args: object, **_kwargs: object) -> object:
|
|
ledger_calls.append("retire")
|
|
if failure_mode == "cross-process-ledger-race":
|
|
raise PhysicalCommandTransitionError(
|
|
"physical command retirement used a stale record checkpoint"
|
|
)
|
|
raise AssertionError("visible stale checkpoint must fail before the ledger CAS")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_async_io)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_async_io)
|
|
monkeypatch.setattr(service, "_probe_control_endpoint", forbidden_sync_io)
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"retire_unavailable_target",
|
|
final_ledger_cas,
|
|
)
|
|
|
|
adapter = XgridsK1PluginFacade(service)
|
|
with pytest.raises(PluginExecutionError) as raised:
|
|
asyncio.run(
|
|
adapter.invoke(
|
|
RuntimeActionInvocation(
|
|
invocation_id=f"retirement-{failure_mode}",
|
|
plugin_id=adapter.plugin_id,
|
|
action_id=ACTION_PHYSICAL_COMMAND_RETIRE_UNAVAILABLE,
|
|
requested_at=datetime.now(UTC),
|
|
parameters={
|
|
**request.model_dump(mode="json"),
|
|
"expected_snapshot_runtime_id": before["snapshot_runtime_id"],
|
|
},
|
|
)
|
|
)
|
|
)
|
|
|
|
assert raised.value.http_status_code == 409
|
|
assert raised.value.reason_code == "physical-command-retirement-stale-checkpoint"
|
|
assert ledger_calls == ([] if failure_mode == "stale-visible-checkpoint" else ["retire"])
|
|
assert service._physical_command_ledger.snapshot().record == before_record # noqa: SLF001
|
|
assert service._operations.snapshot() == before_operations # noqa: SLF001
|
|
assert service._application_control_process_lease_holders == set() # noqa: SLF001
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
assert io_calls == []
|
|
|
|
|
|
def test_operator_retirement_is_local_only_revokes_old_authority_and_keeps_mode_draft(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
_select_connection_mode(service, "quick-connect")
|
|
_set_scanned_devices(
|
|
service,
|
|
[
|
|
{
|
|
"device_id": "retired-k1",
|
|
"name": "Original K1",
|
|
"rssi": -40,
|
|
"address": None,
|
|
"connectable": True,
|
|
"likely_k1": True,
|
|
},
|
|
{
|
|
"device_id": "replacement-k1",
|
|
"name": "Replacement K1",
|
|
"rssi": -45,
|
|
"address": None,
|
|
"connectable": True,
|
|
"likely_k1": True,
|
|
},
|
|
],
|
|
)
|
|
binding = _seed_supervised_connection(
|
|
service,
|
|
transport_ref="RETIRED-K1",
|
|
connection_mode="bridge",
|
|
)
|
|
request = _seed_unresolved_physical_stop_for_retirement(service, binding)
|
|
with service._lock: # noqa: SLF001
|
|
# CoreBluetooth UUID aliases compare case-insensitively for fencing,
|
|
# while the durable retirement CAS keeps its original exact spelling.
|
|
service._selected_device_id = "retired-k1" # noqa: SLF001
|
|
_seed_terminal_network_attempt(service)
|
|
before = service.state()
|
|
before_operations = service._operations.snapshot() # noqa: SLF001
|
|
before_generation = service._ble_discovery_generation # noqa: SLF001
|
|
before_devices = list(service._devices) # noqa: SLF001
|
|
io_calls: list[str] = []
|
|
|
|
async def forbidden_io(*_args: object, **_kwargs: object) -> object:
|
|
io_calls.append("device-or-network-io")
|
|
raise AssertionError("operator retirement must be local-only")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_io)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_io)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_probe_control_endpoint",
|
|
lambda *_args, **_kwargs: io_calls.append("tcp-probe"),
|
|
)
|
|
|
|
assert before["physical_command"]["operator_retirement"]["allowed"] is True
|
|
retirement_action = before["connection_policy"]["actions"]["retire-unavailable-physical-target"]
|
|
assert retirement_action == {
|
|
"allowed": True,
|
|
"reason_codes": [],
|
|
"target_source": "durable-physical-command",
|
|
"required_transport_ref": "RETIRED-K1",
|
|
"requires_live_gatt_validation": False,
|
|
"physical_command_allowed": False,
|
|
"physical_outcome": "unknown",
|
|
"device_write_performed": False,
|
|
"automatic_retry": False,
|
|
}
|
|
assert before["connection_policy"]["recommended_action"] == (
|
|
"retire-unavailable-physical-target"
|
|
)
|
|
assert before["connection_attempt"]["safe_next_action"] == (
|
|
"retire-unavailable-physical-target"
|
|
)
|
|
result = service.retire_unavailable_physical_command(request)
|
|
|
|
physical = result["physical_command"]
|
|
assert physical["record"]["resolution"] == "operator-retired-outcome-unknown"
|
|
assert physical["operator_retirement"]["physical_outcome"] == "unknown"
|
|
assert physical["operator_retirement"]["allowed"] is False
|
|
assert result["desired_connection_mode"] == "quick-connect"
|
|
assert result["selected_device_id"] is None
|
|
assert result["k1_ip"] is None
|
|
assert result["connection_lifecycle"]["active_binding"] is None
|
|
assert result["connection_policy"]["facts"]["retired_transport_refs"] == ["RETIRED-K1"]
|
|
assert result["connection_policy"]["facts"]["eligible_fresh_transport_refs"] == [
|
|
"replacement-k1"
|
|
]
|
|
assert (
|
|
result["connection_policy"]["actions"]["retire-unavailable-physical-target"]["allowed"]
|
|
is False
|
|
)
|
|
assert result["connection_policy"]["actions"]["scan-ble"]["allowed"] is True
|
|
assert result["connection_attempt"]["safe_next_action"] == ("scan-select-connect")
|
|
supervisor = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
assert supervisor.intent is not None
|
|
assert supervisor.intent.intent_id == ("physical-retirement-operator-retirement-stable-request")
|
|
assert supervisor.intent.requested_mode == "quick-connect"
|
|
assert supervisor.authority.control_allowed is False
|
|
assert service._operations.snapshot() == before_operations # noqa: SLF001
|
|
assert service._ble_discovery_generation == before_generation # noqa: SLF001
|
|
assert service._devices == before_devices # noqa: SLF001
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
assert io_calls == []
|
|
|
|
|
|
def test_retired_exact_fresh_candidate_reopens_locally_for_explicit_verify_only(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(
|
|
service,
|
|
transport_ref=DURABLE_K1_UUID,
|
|
connection_mode="bridge",
|
|
)
|
|
retirement = _seed_unresolved_physical_stop_for_retirement(service, binding)
|
|
service.retire_unavailable_physical_command(retirement)
|
|
before_operations = service._operations.snapshot() # noqa: SLF001
|
|
before_runtime_starts = list(runtime.start_calls)
|
|
before_runtime_stops = runtime.stop_calls
|
|
|
|
# A row retained from before retirement is not fresh enough to reopen it.
|
|
_set_scanned_k1(service, device_id=DURABLE_K1_UUID.lower())
|
|
stale = service.state()["physical_command"]["operator_reconciliation_reopen"]
|
|
assert stale["allowed"] is False
|
|
assert "physical-command-reconciliation-reopen-target-not-observed" in stale["reason_codes"]
|
|
|
|
with service._lock: # noqa: SLF001
|
|
service._ble_discovery_generation += 1 # noqa: SLF001
|
|
_set_scanned_k1(service, device_id=DURABLE_K1_UUID.lower())
|
|
before = service.state()
|
|
projection = before["physical_command"]["operator_reconciliation_reopen"]
|
|
assert projection == {
|
|
"allowed": True,
|
|
"reason_codes": [],
|
|
"expected_revision": retirement.expected_revision + 1,
|
|
"expected_retirement_id": retirement.retirement_id,
|
|
"expected_transport_ref": DURABLE_K1_UUID.lower(),
|
|
"expected_discovery_generation": service._ble_discovery_generation, # noqa: SLF001
|
|
"expected_desired_mode": "bridge",
|
|
"expected_desired_mode_revision": 0,
|
|
"device_io_performed": False,
|
|
"automatic_retry": False,
|
|
}
|
|
io_calls: list[str] = []
|
|
|
|
async def forbidden_async_io(*_args: object, **_kwargs: object) -> object:
|
|
io_calls.append("ble-or-wifi")
|
|
raise AssertionError("reopen must remain local-only")
|
|
|
|
def forbidden_sync_io(*_args: object, **_kwargs: object) -> object:
|
|
io_calls.append("mqtt-tcp-monitor")
|
|
raise AssertionError("reopen must remain local-only")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_async_io)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_async_io)
|
|
monkeypatch.setattr(service, "_probe_control_endpoint", forbidden_sync_io)
|
|
reopen_request = ReopenRetiredPhysicalCommandReconciliationRequest(
|
|
reopening_id="reopening-explicit-fresh-row",
|
|
expected_revision=projection["expected_revision"],
|
|
expected_retirement_id=projection["expected_retirement_id"],
|
|
expected_transport_ref=projection["expected_transport_ref"],
|
|
expected_discovery_generation=projection["expected_discovery_generation"],
|
|
expected_desired_mode=projection["expected_desired_mode"],
|
|
expected_desired_mode_revision=projection["expected_desired_mode_revision"],
|
|
operator_confirmed=True,
|
|
reason="device-returned-for-explicit-reconciliation",
|
|
)
|
|
result = service.reopen_retired_physical_command_reconciliation(reopen_request)
|
|
# A lost HTTP response is retried against the durable audit, not against
|
|
# the now-consumed projection. The retry must return usable state without
|
|
# appending another reopen or reacquiring BLE/network ownership.
|
|
retried = service.reopen_retired_physical_command_reconciliation(reopen_request)
|
|
|
|
physical = result["physical_command"]
|
|
assert physical["status"] == "unresolved"
|
|
assert physical["requires_reconciliation"] is True
|
|
assert physical["record"]["stage"] == "observing"
|
|
assert physical["record"]["resolution"] is None
|
|
assert physical["record"]["operator_retirements"][0]["retirement_id"] == (
|
|
retirement.retirement_id
|
|
)
|
|
assert (
|
|
physical["record"]["operator_reconciliation_reopens"][0]["reopening_id"]
|
|
== "reopening-explicit-fresh-row"
|
|
)
|
|
assert len(retried["physical_command"]["record"]["operator_reconciliation_reopens"]) == 1
|
|
assert (
|
|
retried["connection_policy"]["actions"]["observe-fresh-device-network"]["allowed"] is True
|
|
)
|
|
assert result["snapshot_runtime_id"] == before["snapshot_runtime_id"]
|
|
assert retried["snapshot_runtime_id"] == before["snapshot_runtime_id"]
|
|
assert result["snapshot_revision"] > before["snapshot_revision"]
|
|
assert (
|
|
service._ble_discovery_generation
|
|
== projection[ # noqa: SLF001
|
|
"expected_discovery_generation"
|
|
]
|
|
)
|
|
assert result["k1_lifecycle_process_lease"] == {
|
|
"held_by_current_service": False,
|
|
"holders": [],
|
|
"process_lease_quarantined": False,
|
|
"reason_code": None,
|
|
"restart_required": False,
|
|
}
|
|
observe_action = result["connection_policy"]["actions"]["observe-fresh-device-network"]
|
|
assert observe_action["allowed"] is True
|
|
assert observe_action["required_transport_ref"] == DURABLE_K1_UUID
|
|
assert observe_action["required_connection_mode"] == "bridge"
|
|
assert result["connection_policy"]["actions"]["provision-fresh-device"]["allowed"] is False
|
|
assert result["connection_policy"]["actions"]["start-acquisition"]["allowed"] is False
|
|
assert result["connection_policy"]["actions"]["stop-acquisition"]["allowed"] is False
|
|
assert result["connection_lifecycle"]["mode_selection"]["allowed"] is False
|
|
assert result["connection_policy"]["facts"]["retired_transport_refs"] == []
|
|
assert (
|
|
DURABLE_K1_UUID.lower()
|
|
in result["connection_policy"]["facts"]["eligible_fresh_transport_refs"]
|
|
)
|
|
assert service._operations.snapshot() == before_operations # noqa: SLF001
|
|
assert runtime.start_calls == before_runtime_starts
|
|
assert runtime.stop_calls == before_runtime_stops
|
|
assert io_calls == []
|
|
|
|
|
|
@pytest.mark.parametrize("observed_session_state", ["ready", "scanning"])
|
|
def test_casefolded_reopened_uuid_uses_actual_fresh_ref_and_verify_reconciles_state(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
observed_session_state: str,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(
|
|
service,
|
|
transport_ref=DURABLE_K1_UUID,
|
|
connection_mode="bridge",
|
|
)
|
|
retirement = _seed_unresolved_physical_stop_for_retirement(service, binding)
|
|
service.retire_unavailable_physical_command(retirement)
|
|
|
|
# Retirement records the durable UUID exactly as originally observed. A
|
|
# later CoreBluetooth scan may return the same UUID with different case.
|
|
_set_scanned_k1(service, device_id=DURABLE_K1_UUID.lower())
|
|
with service._lock: # noqa: SLF001
|
|
service._ble_discovery_generation += 1 # noqa: SLF001
|
|
_set_scanned_k1(service, device_id=DURABLE_K1_UUID.lower())
|
|
projection = service._physical_operator_reconciliation_reopen_projection() # noqa: SLF001
|
|
reopened = service.reopen_retired_physical_command_reconciliation(
|
|
ReopenRetiredPhysicalCommandReconciliationRequest(
|
|
reopening_id="reopening-casefolded-end-to-end",
|
|
expected_revision=projection["expected_revision"],
|
|
expected_retirement_id=projection["expected_retirement_id"],
|
|
expected_transport_ref=projection["expected_transport_ref"],
|
|
expected_discovery_generation=projection["expected_discovery_generation"],
|
|
expected_desired_mode=projection["expected_desired_mode"],
|
|
expected_desired_mode_revision=projection["expected_desired_mode_revision"],
|
|
operator_confirmed=True,
|
|
reason="device-returned-for-explicit-reconciliation",
|
|
)
|
|
)
|
|
observation = reopened["connection_policy"]["actions"]["observe-fresh-device-network"]
|
|
assert observation["allowed"] is True
|
|
assert observation["required_transport_ref"] == DURABLE_K1_UUID
|
|
|
|
actual_ref = DURABLE_K1_UUID.lower()
|
|
capture = _SYNTHETIC_SCAN_CAPTURES[actual_ref]
|
|
capture_calls: list[str] = []
|
|
status_reads: list[str] = []
|
|
forbidden_mutations: list[str] = []
|
|
|
|
def capture_actual(device_id: str) -> facade_module.CapturedDiscoveredDevice | None:
|
|
capture_calls.append(device_id)
|
|
return capture if device_id == actual_ref else None
|
|
|
|
async def read_status(
|
|
device_id: str,
|
|
*,
|
|
captured_device: facade_module.CapturedDiscoveredDevice | None,
|
|
on_gatt_validated: Callable[[facade_module.CapturedDiscoveredDevice], None],
|
|
**_: object,
|
|
) -> dict[str, Any]:
|
|
status_reads.append(device_id)
|
|
assert captured_device is capture
|
|
on_gatt_validated(capture)
|
|
return _wifi_status_read("10.255.254.77", device_id=device_id)
|
|
|
|
async def forbidden_provision(*_: object, **__: object) -> object:
|
|
forbidden_mutations.append("wifi")
|
|
raise AssertionError("explicit Verify must not provision Wi-Fi")
|
|
|
|
monkeypatch.setattr(facade_module, "capture_discovered_device", capture_actual)
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", read_status)
|
|
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)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
|
|
coordinator = service._physical_command_coordinator # noqa: SLF001
|
|
|
|
async def bootstrap_ready_proof(
|
|
bound_service: XgridsK1CompatibilityService,
|
|
*,
|
|
parent_operation_id: str | None,
|
|
connection_mode: facade_module.ConnectionMode,
|
|
inspection_only: bool = False,
|
|
) -> None:
|
|
await _synthetic_prestart_control_bootstrap(
|
|
bound_service,
|
|
parent_operation_id=parent_operation_id,
|
|
connection_mode=connection_mode,
|
|
inspection_only=inspection_only,
|
|
)
|
|
verified = bound_service._application_control_session.snapshot().get( # noqa: SLF001
|
|
"verified_control"
|
|
)
|
|
assert isinstance(verified, dict)
|
|
coordinator.application_response(
|
|
ApplicationMqttResponseEvidence(
|
|
operation_key="bootstrap:casefold:DeviceInfoRequest",
|
|
response_topic="lixel/application/response/device_info",
|
|
payload_sha256="9" * 64,
|
|
modeling_action=None,
|
|
result_code=None,
|
|
success=None,
|
|
observed_at_utc="2026-08-10T12:00:00.000Z",
|
|
)
|
|
)
|
|
coordinator.bind_control_session(
|
|
PhysicalCommandRuntimeBinding(
|
|
vendor_device_id_sha256=hashlib.sha256(b"retirement-vendor").hexdigest(),
|
|
device_serial_sha256=hashlib.sha256(b"retirement-serial").hexdigest(),
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
intent_id=str(verified["intent_id"]),
|
|
transport_ref=str(verified["transport_ref"]),
|
|
connection_mode=verified["connection_mode"], # type: ignore[arg-type]
|
|
target_ipv4=str(verified["target_ipv4"]),
|
|
target_port=int(verified["target_port"]),
|
|
host_path_epoch=int(verified["host_path_epoch"]),
|
|
control_session_id=str(verified["control_session_id"]),
|
|
producer_generation=int(verified["producer_generation"]),
|
|
)
|
|
)
|
|
coordinator.device_status(
|
|
ApplicationMqttDeviceStatusEvidence(
|
|
vendor_device_id_sha256=hashlib.sha256(b"retirement-vendor").hexdigest(),
|
|
device_serial_sha256=hashlib.sha256(b"retirement-serial").hexdigest(),
|
|
session_state=observed_session_state,
|
|
session_state_code=(
|
|
MODELING_STATE_BASE + (300 if observed_session_state == "ready" else 302)
|
|
),
|
|
project_bound=observed_session_state == "scanning",
|
|
project_id_sha256=("5" * 64 if observed_session_state == "scanning" else None),
|
|
init_ready=observed_session_state == "scanning",
|
|
status_message_sha256="8" * 64,
|
|
mqtt_retained=False,
|
|
observed_at_utc="2026-08-10T12:00:01.000Z",
|
|
)
|
|
)
|
|
bound_service._acquire_application_control_process_lease() # noqa: SLF001
|
|
|
|
service._bootstrap_prestart_control_ready_owned = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
bootstrap_ready_proof,
|
|
service,
|
|
)
|
|
result = asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
# The frontend follows the server policy, whose durable
|
|
# physical recovery target retains the original uppercase
|
|
# spelling. Backend lookup must resolve that to the current
|
|
# lowercase advert before any native BLE call.
|
|
device_id=observation["required_transport_ref"],
|
|
source="fresh-scan",
|
|
compatibility_attestation=ATTESTATION,
|
|
operation_id="op-00000000-0000-4000-8000-000000001240",
|
|
expected_discovery_generation=projection["expected_discovery_generation"],
|
|
)
|
|
)
|
|
)
|
|
|
|
assert capture_calls == [actual_ref]
|
|
assert status_reads == [actual_ref]
|
|
assert result["selected_device_id"] == actual_ref
|
|
assert result["physical_command"]["record"]["resolution"] == (
|
|
"physical-standby-observed"
|
|
if observed_session_state == "ready"
|
|
else "physical-active-observed"
|
|
)
|
|
assert (
|
|
result["physical_command"]["record"]["operator_retirements"][0]["retired_transport_ref"]
|
|
== DURABLE_K1_UUID
|
|
)
|
|
assert (
|
|
result["physical_command"]["record"]["operator_reconciliation_reopens"][0][
|
|
"reopened_transport_ref"
|
|
]
|
|
== actual_ref
|
|
)
|
|
assert forbidden_mutations == []
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
if observed_session_state == "scanning":
|
|
assert result["connection_policy"]["actions"]["provision-fresh-device"]["allowed"] is False
|
|
assert result["connection_policy"]["actions"]["start-acquisition"]["allowed"] is False
|
|
assert result["application_control_session"]["can_stop"] is True
|
|
assert result["connection_policy"]["actions"]["stop-acquisition"]["allowed"] is True, (
|
|
result["connection_policy"]["actions"]["stop-acquisition"]
|
|
)
|
|
assert result["acquisition"]["result"]["recovery_only"] is True
|
|
|
|
|
|
def _retired_fresh_reopen_request(
|
|
service: XgridsK1CompatibilityService,
|
|
*,
|
|
reopening_id: str,
|
|
connection_mode: facade_module.ConnectionMode = "bridge",
|
|
) -> ReopenRetiredPhysicalCommandReconciliationRequest:
|
|
if connection_mode != "bridge":
|
|
_select_connection_mode(service, connection_mode)
|
|
binding = _seed_supervised_connection(
|
|
service,
|
|
transport_ref=DURABLE_K1_UUID,
|
|
connection_mode=connection_mode,
|
|
)
|
|
retirement = _seed_unresolved_physical_stop_for_retirement(service, binding)
|
|
service.retire_unavailable_physical_command(retirement)
|
|
with service._lock: # noqa: SLF001
|
|
service._ble_discovery_generation += 1 # noqa: SLF001
|
|
_set_scanned_k1(service, device_id=DURABLE_K1_UUID.lower())
|
|
projection = service._physical_operator_reconciliation_reopen_projection() # noqa: SLF001
|
|
assert projection["allowed"] is True
|
|
return ReopenRetiredPhysicalCommandReconciliationRequest(
|
|
reopening_id=reopening_id,
|
|
expected_revision=projection["expected_revision"],
|
|
expected_retirement_id=projection["expected_retirement_id"],
|
|
expected_transport_ref=projection["expected_transport_ref"],
|
|
expected_discovery_generation=projection["expected_discovery_generation"],
|
|
expected_desired_mode=projection["expected_desired_mode"],
|
|
expected_desired_mode_revision=projection["expected_desired_mode_revision"],
|
|
operator_confirmed=True,
|
|
reason="device-returned-for-explicit-reconciliation",
|
|
)
|
|
|
|
|
|
def test_physical_reopen_final_checkpoint_conflict_is_http_409_without_device_io(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
request = _retired_fresh_reopen_request(
|
|
service,
|
|
reopening_id="reopening-cross-process-ledger-race",
|
|
)
|
|
before = service.state()
|
|
before_record = service._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
before_operations = service._operations.snapshot() # noqa: SLF001
|
|
io_calls: list[str] = []
|
|
ledger_calls: list[str] = []
|
|
|
|
async def forbidden_async_io(*_args: object, **_kwargs: object) -> object:
|
|
io_calls.append("ble-or-wifi")
|
|
raise AssertionError("reopen checkpoint conflict must not reach device I/O")
|
|
|
|
def forbidden_sync_io(*_args: object, **_kwargs: object) -> object:
|
|
io_calls.append("mqtt-tcp-monitor")
|
|
raise AssertionError("reopen checkpoint conflict must not probe the device")
|
|
|
|
def conflicting_final_cas(*_args: object, **_kwargs: object) -> object:
|
|
ledger_calls.append("reopen")
|
|
raise PhysicalCommandTransitionError(
|
|
"physical command reconciliation reopen used a stale record revision"
|
|
)
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_async_io)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_async_io)
|
|
monkeypatch.setattr(service, "_probe_control_endpoint", forbidden_sync_io)
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"reopen_retired_reconciliation",
|
|
conflicting_final_cas,
|
|
)
|
|
|
|
adapter = XgridsK1PluginFacade(service)
|
|
with pytest.raises(PluginExecutionError) as raised:
|
|
asyncio.run(
|
|
adapter.invoke(
|
|
RuntimeActionInvocation(
|
|
invocation_id="reopening-cross-process-ledger-race",
|
|
plugin_id=adapter.plugin_id,
|
|
action_id=(ACTION_PHYSICAL_COMMAND_REOPEN_RETIRED_RECONCILIATION),
|
|
requested_at=datetime.now(UTC),
|
|
parameters={
|
|
**request.model_dump(mode="json"),
|
|
"expected_snapshot_runtime_id": before["snapshot_runtime_id"],
|
|
},
|
|
)
|
|
)
|
|
)
|
|
|
|
assert raised.value.http_status_code == 409
|
|
assert raised.value.reason_code == ("physical-command-reconciliation-reopen-stale-checkpoint")
|
|
assert ledger_calls == ["reopen"]
|
|
assert service._physical_command_ledger.snapshot().record == before_record # noqa: SLF001
|
|
assert service._operations.snapshot() == before_operations # noqa: SLF001
|
|
assert service._application_control_process_lease_holders == set() # noqa: SLF001
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
assert io_calls == []
|
|
|
|
|
|
def test_reopen_projection_rejects_a_different_local_mode_draft_without_mutation(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
request = _retired_fresh_reopen_request(
|
|
service,
|
|
reopening_id="reopening-mode-mismatch",
|
|
)
|
|
before_record = service._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
before_operations = service._operations.snapshot() # noqa: SLF001
|
|
before_generation = service._ble_discovery_generation # noqa: SLF001
|
|
|
|
switched_revision = _select_connection_mode(service, "quick-connect")
|
|
projection = service._physical_operator_reconciliation_reopen_projection() # noqa: SLF001
|
|
|
|
assert switched_revision == 1
|
|
assert projection["allowed"] is False
|
|
assert projection["reason_codes"] == ["physical-command-reconciliation-reopen-mode-mismatch"]
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as blocked:
|
|
service.reopen_retired_physical_command_reconciliation(request)
|
|
|
|
assert blocked.value.reason_code == ("physical-command-reconciliation-reopen-mode-mismatch")
|
|
assert service._physical_command_ledger.snapshot().record == before_record # noqa: SLF001
|
|
assert service._operations.snapshot() == before_operations # noqa: SLF001
|
|
assert service._ble_discovery_generation == before_generation # noqa: SLF001
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
|
|
|
|
def test_reopen_rejects_desired_mode_revision_aba_without_consuming_checkpoint(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
stale_request = _retired_fresh_reopen_request(
|
|
service,
|
|
reopening_id="reopening-mode-revision-aba",
|
|
)
|
|
before_record = service._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
before_operations = service._operations.snapshot() # noqa: SLF001
|
|
|
|
_select_connection_mode(service, "quick-connect")
|
|
current_revision = _select_connection_mode(service, "bridge")
|
|
projection = service._physical_operator_reconciliation_reopen_projection() # noqa: SLF001
|
|
|
|
assert current_revision == 2
|
|
assert projection["allowed"] is True
|
|
assert projection["expected_desired_mode"] == "bridge"
|
|
assert projection["expected_desired_mode_revision"] == 2
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as stale:
|
|
service.reopen_retired_physical_command_reconciliation(stale_request)
|
|
|
|
assert stale.value.reason_code == ("physical-command-reconciliation-reopen-stale-checkpoint")
|
|
assert service._physical_command_ledger.snapshot().record == before_record # noqa: SLF001
|
|
assert service._operations.snapshot() == before_operations # noqa: SLF001
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
|
|
|
|
def test_reopen_freezes_mode_draft_until_physical_observation_terminalizes(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
request = _retired_fresh_reopen_request(
|
|
service,
|
|
reopening_id="reopening-freezes-mode-draft",
|
|
)
|
|
reopened = service.reopen_retired_physical_command_reconciliation(request)
|
|
reopened_record = service._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
|
|
assert reopened["physical_command"]["requires_reconciliation"] is True
|
|
assert reopened["connection_lifecycle"]["mode_selection"]["allowed"] is False
|
|
assert (
|
|
"connection-mode-selection-physical-state-unsafe"
|
|
in reopened["connection_lifecycle"]["mode_selection"]["reason_codes"]
|
|
)
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as blocked:
|
|
_select_connection_mode(service, "quick-connect")
|
|
|
|
assert blocked.value.reason_code == ("connection-mode-selection-physical-state-unsafe")
|
|
assert service._physical_command_ledger.snapshot().record == reopened_record # noqa: SLF001
|
|
assert service.state()["desired_connection_mode"] == "bridge"
|
|
assert service.state()["desired_connection_mode_revision"] == 0
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
|
|
|
|
def test_restarted_peer_can_align_only_to_pending_reopen_mode_without_io(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
owner, _ = service_with_fake_runtime(tmp_path)
|
|
request = _retired_fresh_reopen_request(
|
|
owner,
|
|
reopening_id="reopening-quick-restart-mode-alignment",
|
|
connection_mode="quick-connect",
|
|
)
|
|
owner.reopen_retired_physical_command_reconciliation(request)
|
|
|
|
restarted, restarted_runtime = service_with_fake_runtime(tmp_path)
|
|
before_record = restarted._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
device_edges: list[str] = []
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"scan",
|
|
lambda *_args, **_kwargs: device_edges.append("scan"),
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"provision_wifi_once",
|
|
lambda *_args, **_kwargs: device_edges.append("network-write"),
|
|
)
|
|
|
|
before = restarted.state()
|
|
assert before["desired_connection_mode"] == "bridge"
|
|
assert before["connection_lifecycle"]["mode_selection"]["allowed"] is True
|
|
aligned = restarted.select_connection_mode(
|
|
DesiredConnectionModeRequest(
|
|
connection_mode="quick-connect",
|
|
expected_revision=before["desired_connection_mode_revision"],
|
|
)
|
|
)
|
|
|
|
assert aligned["desired_connection_mode"] == "quick-connect"
|
|
assert aligned["desired_connection_mode_revision"] == 1
|
|
assert aligned["connection_lifecycle"]["mode_selection"]["allowed"] is False
|
|
assert restarted._physical_command_ledger.snapshot().record == before_record # noqa: SLF001
|
|
assert restarted_runtime.start_calls == []
|
|
assert restarted_runtime.stop_calls == 0
|
|
assert device_edges == []
|
|
|
|
|
|
def _install_real_retirement_ready_bootstrap(
|
|
service: XgridsK1CompatibilityService,
|
|
) -> None:
|
|
"""Feed exact fresh DeviceInfo/READY evidence to the real coordinator."""
|
|
|
|
coordinator = service._physical_command_coordinator # noqa: SLF001
|
|
|
|
async def bootstrap_ready_proof(
|
|
bound_service: XgridsK1CompatibilityService,
|
|
*,
|
|
parent_operation_id: str | None,
|
|
connection_mode: facade_module.ConnectionMode,
|
|
inspection_only: bool = False,
|
|
) -> None:
|
|
await _synthetic_prestart_control_bootstrap(
|
|
bound_service,
|
|
parent_operation_id=parent_operation_id,
|
|
connection_mode=connection_mode,
|
|
inspection_only=inspection_only,
|
|
)
|
|
verified = bound_service._application_control_session.snapshot().get( # noqa: SLF001
|
|
"verified_control"
|
|
)
|
|
assert isinstance(verified, dict)
|
|
vendor_hash = hashlib.sha256(b"retirement-vendor").hexdigest()
|
|
serial_hash = hashlib.sha256(b"retirement-serial").hexdigest()
|
|
coordinator.application_response(
|
|
ApplicationMqttResponseEvidence(
|
|
operation_key="bootstrap:retirement:DeviceInfoRequest",
|
|
response_topic="lixel/application/response/device_info",
|
|
payload_sha256="7" * 64,
|
|
modeling_action=None,
|
|
result_code=None,
|
|
success=None,
|
|
observed_at_utc="2026-08-10T12:10:00.000Z",
|
|
)
|
|
)
|
|
coordinator.bind_control_session(
|
|
PhysicalCommandRuntimeBinding(
|
|
vendor_device_id_sha256=vendor_hash,
|
|
device_serial_sha256=serial_hash,
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
intent_id=str(verified["intent_id"]),
|
|
transport_ref=str(verified["transport_ref"]),
|
|
connection_mode=verified["connection_mode"], # type: ignore[arg-type]
|
|
target_ipv4=str(verified["target_ipv4"]),
|
|
target_port=int(verified["target_port"]),
|
|
host_path_epoch=int(verified["host_path_epoch"]),
|
|
control_session_id=str(verified["control_session_id"]),
|
|
producer_generation=int(verified["producer_generation"]),
|
|
)
|
|
)
|
|
coordinator.device_status(
|
|
ApplicationMqttDeviceStatusEvidence(
|
|
vendor_device_id_sha256=vendor_hash,
|
|
device_serial_sha256=serial_hash,
|
|
session_state="ready",
|
|
session_state_code=MODELING_STATE_BASE + 300,
|
|
project_bound=False,
|
|
project_id_sha256=None,
|
|
init_ready=False,
|
|
status_message_sha256="6" * 64,
|
|
mqtt_retained=False,
|
|
observed_at_utc="2026-08-10T12:10:01.000Z",
|
|
)
|
|
)
|
|
bound_service._acquire_application_control_process_lease() # noqa: SLF001
|
|
|
|
service._bootstrap_prestart_control_ready_owned = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
bootstrap_ready_proof,
|
|
service,
|
|
)
|
|
|
|
|
|
def test_reopened_verify_network_unavailable_stays_unresolved_and_can_retry_exactly(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
reopen_request = _retired_fresh_reopen_request(
|
|
service,
|
|
reopening_id="reopening-network-unavailable-retry",
|
|
)
|
|
reopened = service.reopen_retired_physical_command_reconciliation(reopen_request)
|
|
observation = reopened["connection_policy"]["actions"]["observe-fresh-device-network"]
|
|
durable_ref = str(observation["required_transport_ref"])
|
|
actual_ref = reopen_request.expected_transport_ref
|
|
capture = _SYNTHETIC_SCAN_CAPTURES[actual_ref]
|
|
observed_addresses: list[str | None] = [None]
|
|
status_reads: list[str] = []
|
|
forbidden_mutations: list[str] = []
|
|
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"capture_discovered_device",
|
|
lambda device_id: capture if device_id == actual_ref else None,
|
|
)
|
|
|
|
async def read_status(
|
|
device_id: str,
|
|
*,
|
|
captured_device: facade_module.CapturedDiscoveredDevice | None,
|
|
on_gatt_validated: Callable[[facade_module.CapturedDiscoveredDevice], None],
|
|
**_: object,
|
|
) -> dict[str, Any]:
|
|
status_reads.append(device_id)
|
|
assert captured_device is capture
|
|
on_gatt_validated(capture)
|
|
return _wifi_status_read(observed_addresses[0], device_id=device_id)
|
|
|
|
async def forbidden_provision(*_: object, **__: object) -> object:
|
|
forbidden_mutations.append("wifi")
|
|
raise AssertionError("network-unavailable Verify must not provision")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", read_status)
|
|
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)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
|
|
with pytest.raises(facade_module.ConnectionVerificationError) as unavailable:
|
|
asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id=durable_ref,
|
|
source="fresh-scan",
|
|
compatibility_attestation=ATTESTATION,
|
|
operation_id="op-00000000-0000-4000-8000-000000001241",
|
|
expected_discovery_generation=(reopen_request.expected_discovery_generation),
|
|
)
|
|
)
|
|
)
|
|
|
|
assert unavailable.value.reason_code == "connection-verify-address-unavailable"
|
|
failed = service.state()
|
|
assert failed["connection_verification"]["status"] == "unreachable"
|
|
assert failed["connection_verification"]["network_reachability"] == ("unreachable")
|
|
assert failed["connection_verification"]["reason_code"] == (
|
|
"connection-verify-address-unavailable"
|
|
)
|
|
assert failed["physical_command"]["status"] == "unresolved"
|
|
assert failed["physical_command"]["requires_reconciliation"] is True
|
|
assert failed["connection_policy"]["facts"]["retired_transport_refs"] == []
|
|
assert failed["physical_command"]["operator_retirement"]["allowed"] is True
|
|
assert failed["connection_policy"]["actions"]["provision-fresh-device"]["allowed"] is False
|
|
assert failed["connection_policy"]["actions"]["start-acquisition"]["allowed"] is False
|
|
assert failed["connection_policy"]["actions"]["stop-acquisition"]["allowed"] is False
|
|
# The disconnected selector was freely editable before this exact reopen.
|
|
# The admitted reopen now freezes that mode CAS until the immediately owned
|
|
# read-only observation terminalizes, so another tab cannot strand it.
|
|
assert failed["connection_lifecycle"]["mode_selection"]["allowed"] is False
|
|
assert (
|
|
"connection-mode-selection-physical-state-unsafe"
|
|
in failed["connection_lifecycle"]["mode_selection"]["reason_codes"]
|
|
)
|
|
assert service._application_control_process_lease_holders == set() # noqa: SLF001
|
|
|
|
# Restoring the old route does not replay anything. The operator can
|
|
# explicitly run the same server-bound observation against the still-fresh
|
|
# exact candidate and reconcile READY.
|
|
observed_addresses[0] = "10.255.254.77"
|
|
_install_real_retirement_ready_bootstrap(service)
|
|
recovered = asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id=durable_ref,
|
|
source="fresh-scan",
|
|
compatibility_attestation=ATTESTATION,
|
|
operation_id="op-00000000-0000-4000-8000-000000001242",
|
|
expected_discovery_generation=(reopen_request.expected_discovery_generation),
|
|
)
|
|
)
|
|
)
|
|
|
|
assert status_reads == [actual_ref, actual_ref]
|
|
assert recovered["physical_command"]["record"]["resolution"] == ("physical-standby-observed")
|
|
assert forbidden_mutations == []
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("blocker", "reason_code"),
|
|
[
|
|
("network", "network-provision-operation-active"),
|
|
("control", "control-session-not-admissible-for-network-change"),
|
|
("lifecycle", "k1-lifecycle-process-lease-control-owned"),
|
|
("ble", "ble-runtime-busy"),
|
|
(
|
|
"nonconnectable",
|
|
"physical-command-reconciliation-reopen-target-not-connectable",
|
|
),
|
|
],
|
|
)
|
|
def test_reopen_projection_blocks_live_owners_before_durable_or_device_action(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
blocker: str,
|
|
reason_code: str,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
request = _retired_fresh_reopen_request(
|
|
service,
|
|
reopening_id=f"reopening-blocked-{blocker}",
|
|
)
|
|
before = service._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
assert before is not None
|
|
native = dict(facade_module.ble_runtime_snapshot())
|
|
if blocker == "network":
|
|
service._provisioning_active = True # noqa: SLF001
|
|
elif blocker == "control":
|
|
service._application_control_session = FakeInteractiveControlSession( # type: ignore[assignment] # noqa: SLF001
|
|
initial_state="workspace-ready"
|
|
)
|
|
elif blocker == "lifecycle":
|
|
service._application_control_process_lease_holders.add("control") # noqa: SLF001
|
|
elif blocker == "ble":
|
|
native["active_operation_kind"] = "scan"
|
|
monkeypatch.setattr(facade_module, "ble_runtime_snapshot", lambda: native)
|
|
elif blocker == "nonconnectable":
|
|
service._devices[0]["connectable"] = False # noqa: SLF001
|
|
else: # pragma: no cover - exhaustive test table
|
|
raise AssertionError(blocker)
|
|
|
|
projection = service._physical_operator_reconciliation_reopen_projection() # noqa: SLF001
|
|
assert projection["allowed"] is False
|
|
assert reason_code in projection["reason_codes"]
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as blocked:
|
|
service.reopen_retired_physical_command_reconciliation(request)
|
|
|
|
assert blocked.value.reason_code == reason_code
|
|
assert service._physical_command_ledger.snapshot().record == before # noqa: SLF001
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
|
|
|
|
@pytest.mark.parametrize("safe_to_retry", [False, True])
|
|
def test_reopen_uses_the_same_failed_control_admission_as_fresh_verify(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
safe_to_retry: bool,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
request = _retired_fresh_reopen_request(
|
|
service,
|
|
reopening_id=f"reopening-failed-control-{safe_to_retry}",
|
|
)
|
|
before = service._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
control = FakeInteractiveControlSession(initial_state="failed")
|
|
base_snapshot = control.snapshot
|
|
|
|
def failed_snapshot() -> dict[str, object]:
|
|
return {
|
|
**base_snapshot(),
|
|
"failure": {
|
|
"safe_to_retry": safe_to_retry,
|
|
"network_change_admissible": False,
|
|
},
|
|
}
|
|
|
|
monkeypatch.setattr(control, "snapshot", failed_snapshot)
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
projection = service._physical_operator_reconciliation_reopen_projection() # noqa: SLF001
|
|
|
|
if not safe_to_retry:
|
|
assert projection["allowed"] is False
|
|
assert "control-session-not-admissible-for-network-change" in projection["reason_codes"]
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as blocked:
|
|
service.reopen_retired_physical_command_reconciliation(request)
|
|
assert blocked.value.reason_code == ("control-session-not-admissible-for-network-change")
|
|
assert service._physical_command_ledger.snapshot().record == before # noqa: SLF001
|
|
else:
|
|
assert projection["allowed"] is True
|
|
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("blocker", "reason_code"),
|
|
[
|
|
("supervisor", "connection-supervisor-closed"),
|
|
("idempotency-unavailable", "network-provisioning-idempotency-unavailable"),
|
|
("idempotency-corrupt", "network-provisioning-idempotency-corrupt"),
|
|
("topology-corrupt", "semantic-topology-store-corrupt"),
|
|
("identity-corrupt", "device-identity-pin-store-corrupt"),
|
|
("network-ledger-corrupt", "network-mutation-ledger-corrupt"),
|
|
("foreign-reconciliation", "reconciliation-target-not-observed"),
|
|
],
|
|
)
|
|
def test_reopen_rejects_unchanged_post_reopen_verify_blockers_before_commit(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
blocker: str,
|
|
reason_code: str,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
request = _retired_fresh_reopen_request(
|
|
service,
|
|
reopening_id=f"reopening-environment-blocked-{blocker}",
|
|
)
|
|
before = service._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
if blocker == "supervisor":
|
|
monkeypatch.setattr(
|
|
service._connection_supervisor, # noqa: SLF001
|
|
"snapshot",
|
|
lambda: SimpleNamespace(closed=True),
|
|
)
|
|
elif blocker == "idempotency-unavailable":
|
|
service._network_provisioning_idempotency_journal = None # noqa: SLF001
|
|
elif blocker == "idempotency-corrupt":
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_network_provisioning_idempotency_public_snapshot",
|
|
lambda: {"status": "corrupt"},
|
|
)
|
|
elif blocker == "topology-corrupt":
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_semantic_topology_public_snapshot",
|
|
lambda: {"status": "corrupt"},
|
|
)
|
|
elif blocker == "identity-corrupt":
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_device_identity_pin_public_snapshot",
|
|
lambda: {"status": "corrupt"},
|
|
)
|
|
elif blocker == "network-ledger-corrupt":
|
|
monkeypatch.setattr(
|
|
service._network_mutation_ledger, # noqa: SLF001
|
|
"snapshot",
|
|
lambda: SimpleNamespace(status="corrupt", record=None),
|
|
)
|
|
elif blocker == "foreign-reconciliation":
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_network_provisioning_idempotency_public_snapshot",
|
|
lambda: {
|
|
"status": "blocked",
|
|
"active_operation_id": "foreign-network-operation",
|
|
"active_action": facade_module.ACTION_NETWORK_PROVISION,
|
|
"active_stage": "unresolved",
|
|
},
|
|
)
|
|
monkeypatch.setattr(
|
|
service._network_mutation_ledger, # noqa: SLF001
|
|
"snapshot",
|
|
lambda: SimpleNamespace(
|
|
status="unresolved",
|
|
record=SimpleNamespace(
|
|
operation_id="foreign-network-operation",
|
|
stage="dispatching",
|
|
resolution=None,
|
|
transport_ref="foreign-transport",
|
|
intended_mode="bridge",
|
|
),
|
|
),
|
|
)
|
|
else: # pragma: no cover - exhaustive parameter table
|
|
raise AssertionError(blocker)
|
|
|
|
projection = service._physical_operator_reconciliation_reopen_projection() # noqa: SLF001
|
|
assert projection["allowed"] is False
|
|
assert reason_code in projection["reason_codes"]
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as blocked:
|
|
service.reopen_retired_physical_command_reconciliation(request)
|
|
|
|
assert blocked.value.reason_code == reason_code
|
|
assert service._physical_command_ledger.snapshot().record == before # noqa: SLF001
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("audit_shape", "reason_code"),
|
|
[
|
|
(
|
|
"different-unresolved-operation",
|
|
"network-provisioning-idempotency-operation-mismatch",
|
|
),
|
|
(
|
|
"unresolved-ledger-without-idempotency",
|
|
"network-provisioning-idempotency-operation-mismatch",
|
|
),
|
|
(
|
|
"active-idempotency-without-ledger",
|
|
"network-provisioning-idempotency-ledger-mismatch",
|
|
),
|
|
(
|
|
"same-operation-prepared-prepared",
|
|
"network-provisioning-idempotency-ledger-mismatch",
|
|
),
|
|
],
|
|
)
|
|
def test_reopen_rejects_unexecutable_network_audit_pairing_before_lease_or_io(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
audit_shape: str,
|
|
reason_code: str,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
request = _retired_fresh_reopen_request(
|
|
service,
|
|
reopening_id=f"reopening-audit-blocked-{audit_shape}",
|
|
)
|
|
operation_id = f"network-audit-{audit_shape}"
|
|
if audit_shape == "different-unresolved-operation":
|
|
_seed_unresolved_network_mutation(
|
|
service,
|
|
operation_id=operation_id,
|
|
idempotency_operation_id=f"{operation_id}-other",
|
|
transport_ref=request.expected_transport_ref,
|
|
)
|
|
elif audit_shape == "unresolved-ledger-without-idempotency":
|
|
prepared = _prepare_network_mutation_for_reopen(
|
|
service,
|
|
operation_id=operation_id,
|
|
transport_ref=request.expected_transport_ref,
|
|
intended_mode="bridge",
|
|
)
|
|
service._network_mutation_ledger.mark_dispatching( # noqa: SLF001
|
|
operation_id,
|
|
expected_revision=prepared.revision,
|
|
)
|
|
elif audit_shape == "active-idempotency-without-ledger":
|
|
_begin_network_idempotency_for_reopen(
|
|
service,
|
|
operation_id=operation_id,
|
|
mark_unresolved=True,
|
|
)
|
|
elif audit_shape == "same-operation-prepared-prepared":
|
|
_prepare_network_mutation_for_reopen(
|
|
service,
|
|
operation_id=operation_id,
|
|
transport_ref=request.expected_transport_ref,
|
|
intended_mode="bridge",
|
|
)
|
|
_begin_network_idempotency_for_reopen(
|
|
service,
|
|
operation_id=operation_id,
|
|
mark_unresolved=False,
|
|
)
|
|
else: # pragma: no cover - exhaustive parameter table
|
|
raise AssertionError(audit_shape)
|
|
|
|
physical_before = service._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
network_before = service._network_mutation_ledger.snapshot() # noqa: SLF001
|
|
idempotency_before = ( # noqa: SLF001
|
|
service._require_network_provisioning_idempotency_journal().snapshot()
|
|
)
|
|
operations_before = service._operations.snapshot() # noqa: SLF001
|
|
lease_calls: list[str] = []
|
|
io_calls: list[str] = []
|
|
|
|
def forbidden_lease(*_args: object, **_kwargs: object) -> None:
|
|
lease_calls.append("lease")
|
|
raise AssertionError("rejected reopen must not acquire the lifecycle lease")
|
|
|
|
async def forbidden_async_io(*_args: object, **_kwargs: object) -> object:
|
|
io_calls.append("device-io")
|
|
raise AssertionError("rejected reopen must not perform device I/O")
|
|
|
|
monkeypatch.setattr(service, "_acquire_k1_lifecycle_process_lease", forbidden_lease)
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_async_io)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_async_io)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_probe_control_endpoint",
|
|
lambda *_args, **_kwargs: io_calls.append("endpoint-io"),
|
|
)
|
|
|
|
projection = service._physical_operator_reconciliation_reopen_projection() # noqa: SLF001
|
|
assert projection["allowed"] is False
|
|
assert reason_code in projection["reason_codes"]
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as blocked:
|
|
service.reopen_retired_physical_command_reconciliation(request)
|
|
|
|
assert blocked.value.reason_code == reason_code
|
|
assert service._physical_command_ledger.snapshot().record == physical_before # noqa: SLF001
|
|
assert service._network_mutation_ledger.snapshot() == network_before # noqa: SLF001
|
|
assert ( # noqa: SLF001
|
|
service._require_network_provisioning_idempotency_journal().snapshot() == idempotency_before
|
|
)
|
|
assert service._operations.snapshot() == operations_before # noqa: SLF001
|
|
assert lease_calls == []
|
|
assert io_calls == []
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
|
|
|
|
def test_reopen_rejects_unresolved_network_mode_conflict_before_lease_or_io(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
request = _retired_fresh_reopen_request(
|
|
service,
|
|
reopening_id="reopening-network-mode-conflict",
|
|
)
|
|
_seed_unresolved_network_mutation(
|
|
service,
|
|
operation_id="network-mode-conflict",
|
|
transport_ref=request.expected_transport_ref.upper(),
|
|
intended_mode="quick-connect",
|
|
)
|
|
physical_before = service._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
network_before = service._network_mutation_ledger.snapshot() # noqa: SLF001
|
|
idempotency_before = ( # noqa: SLF001
|
|
service._require_network_provisioning_idempotency_journal().snapshot()
|
|
)
|
|
lease_calls: list[str] = []
|
|
io_calls: list[str] = []
|
|
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_acquire_k1_lifecycle_process_lease",
|
|
lambda *_args, **_kwargs: lease_calls.append("lease"),
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"read_wifi_status_once",
|
|
lambda *_args, **_kwargs: io_calls.append("gatt-read"),
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"provision_wifi_once",
|
|
lambda *_args, **_kwargs: io_calls.append("gatt-write"),
|
|
)
|
|
|
|
projection = service._physical_operator_reconciliation_reopen_projection() # noqa: SLF001
|
|
assert projection["allowed"] is False
|
|
assert "reconciliation-target-mode-mismatch" in projection["reason_codes"]
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as blocked:
|
|
service.reopen_retired_physical_command_reconciliation(request)
|
|
|
|
assert blocked.value.reason_code == "reconciliation-target-mode-mismatch"
|
|
assert service._physical_command_ledger.snapshot().record == physical_before # noqa: SLF001
|
|
assert service._network_mutation_ledger.snapshot() == network_before # noqa: SLF001
|
|
assert ( # noqa: SLF001
|
|
service._require_network_provisioning_idempotency_journal().snapshot() == idempotency_before
|
|
)
|
|
assert lease_calls == []
|
|
assert io_calls == []
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"audit_shape",
|
|
[
|
|
"prepared-without-idempotency",
|
|
"prepared-with-matching-unresolved-idempotency",
|
|
"resolved-not-dispatched-with-matching-unresolved-idempotency",
|
|
"resolved-target-observed-with-matching-unresolved-idempotency",
|
|
],
|
|
)
|
|
def test_reopen_admits_network_audit_shapes_verify_can_repair_exactly(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
audit_shape: str,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
request = _retired_fresh_reopen_request(
|
|
service,
|
|
reopening_id=f"reopening-repairable-{audit_shape}",
|
|
)
|
|
operation_id = f"network-repairable-{audit_shape}"
|
|
prepared = _prepare_network_mutation_for_reopen(
|
|
service,
|
|
operation_id=operation_id,
|
|
transport_ref=request.expected_transport_ref,
|
|
# PREPARED is deterministically closed as not-dispatched before Verify
|
|
# chooses the physical recovery mode, so this difference is repairable.
|
|
intended_mode=("quick-connect" if audit_shape.startswith("prepared-") else "bridge"),
|
|
)
|
|
if "matching-unresolved-idempotency" in audit_shape:
|
|
_begin_network_idempotency_for_reopen(
|
|
service,
|
|
operation_id=operation_id,
|
|
mark_unresolved=True,
|
|
)
|
|
if audit_shape.startswith("resolved-not-dispatched"):
|
|
service._network_mutation_ledger.resolve( # noqa: SLF001
|
|
operation_id,
|
|
expected_revision=prepared.revision,
|
|
resolution="not-dispatched",
|
|
)
|
|
elif audit_shape.startswith("resolved-target-observed"):
|
|
dispatching = service._network_mutation_ledger.mark_dispatching( # noqa: SLF001
|
|
operation_id,
|
|
expected_revision=prepared.revision,
|
|
)
|
|
service._network_mutation_ledger.resolve( # noqa: SLF001
|
|
operation_id,
|
|
expected_revision=dispatching.revision,
|
|
resolution="target-observed",
|
|
observation=NetworkStatusEvidence(
|
|
mode="WIFI_CLIENT",
|
|
ipv4="192.168.68.51",
|
|
status_code=1,
|
|
reserved=0,
|
|
),
|
|
)
|
|
|
|
network_before = service._network_mutation_ledger.snapshot() # noqa: SLF001
|
|
idempotency_before = ( # noqa: SLF001
|
|
service._require_network_provisioning_idempotency_journal().snapshot()
|
|
)
|
|
io_calls: list[str] = []
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"read_wifi_status_once",
|
|
lambda *_args, **_kwargs: io_calls.append("gatt-read"),
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"provision_wifi_once",
|
|
lambda *_args, **_kwargs: io_calls.append("gatt-write"),
|
|
)
|
|
|
|
projection = service._physical_operator_reconciliation_reopen_projection() # noqa: SLF001
|
|
assert projection["allowed"] is True, projection
|
|
reopened = service.reopen_retired_physical_command_reconciliation(request)
|
|
|
|
assert reopened["physical_command"]["requires_reconciliation"] is True
|
|
assert (
|
|
reopened["connection_policy"]["actions"]["observe-fresh-device-network"]["allowed"] is True
|
|
)
|
|
assert service._network_mutation_ledger.snapshot() == network_before # noqa: SLF001
|
|
assert ( # noqa: SLF001
|
|
service._require_network_provisioning_idempotency_journal().snapshot() == idempotency_before
|
|
)
|
|
assert io_calls == []
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
|
|
|
|
def test_fresh_observation_requires_network_and_physical_recovery_same_target(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_devices(
|
|
service,
|
|
[
|
|
{"device_id": "physical-k1", "connectable": True},
|
|
{"device_id": "network-k1", "connectable": True},
|
|
],
|
|
)
|
|
_seed_unresolved_network_mutation(
|
|
service,
|
|
operation_id="network-other-fresh-target",
|
|
transport_ref="network-k1",
|
|
intended_mode="bridge",
|
|
)
|
|
|
|
policy = _project_connection_policy_for_test(
|
|
service,
|
|
physical_command=_resolved_active_physical_snapshot(
|
|
transport_ref="physical-k1",
|
|
connection_mode="bridge",
|
|
),
|
|
)
|
|
|
|
observation = policy["actions"]["observe-fresh-device-network"]
|
|
assert observation["allowed"] is False
|
|
assert "reconciliation-target-physical-recovery-mismatch" in observation["reason_codes"]
|
|
assert observation["required_transport_ref"] == "physical-k1"
|
|
|
|
|
|
def test_reopen_rejects_discovery_generation_drift_before_durable_commit(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
request = _retired_fresh_reopen_request(
|
|
service,
|
|
reopening_id="reopening-stale-discovery-generation",
|
|
)
|
|
before = service._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
with service._lock: # noqa: SLF001
|
|
service._ble_discovery_generation += 1 # noqa: SLF001
|
|
_set_scanned_k1(service, device_id=DURABLE_K1_UUID.lower())
|
|
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as stale:
|
|
service.reopen_retired_physical_command_reconciliation(request)
|
|
|
|
assert stale.value.reason_code == ("physical-command-reconciliation-reopen-stale-checkpoint")
|
|
assert service._physical_command_ledger.snapshot().record == before # noqa: SLF001
|
|
assert service._application_control_process_lease_holders == set() # noqa: SLF001
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
|
|
|
|
def test_reopen_two_tab_lost_response_retry_uses_one_durable_audit(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
first, _ = service_with_fake_runtime(tmp_path)
|
|
request = _retired_fresh_reopen_request(
|
|
first,
|
|
reopening_id="reopening-two-tab-lost-response",
|
|
)
|
|
second, second_runtime = service_with_fake_runtime(tmp_path)
|
|
with second._lock: # noqa: SLF001
|
|
second._ble_discovery_generation = request.expected_discovery_generation # noqa: SLF001
|
|
_set_scanned_k1(second, device_id=request.expected_transport_ref)
|
|
|
|
committed = first.reopen_retired_physical_command_reconciliation(request)
|
|
monkeypatch.setattr(
|
|
second,
|
|
"_acquire_k1_lifecycle_process_lease",
|
|
lambda *_args, **_kwargs: (_ for _ in ()).throw(
|
|
AssertionError("durable lost-response retry must not reacquire the lease")
|
|
),
|
|
)
|
|
observed = second.reopen_retired_physical_command_reconciliation(request)
|
|
|
|
assert committed["physical_command"]["status"] == "unresolved"
|
|
reopens = observed["physical_command"]["record"]["operator_reconciliation_reopens"]
|
|
assert [item["reopening_id"] for item in reopens] == ["reopening-two-tab-lost-response"]
|
|
assert (
|
|
observed["connection_policy"]["actions"]["observe-fresh-device-network"]["allowed"] is True
|
|
)
|
|
assert second_runtime.start_calls == []
|
|
assert second_runtime.stop_calls == 0
|
|
|
|
|
|
def test_reopen_quick_restart_retry_is_durable_and_does_not_require_local_draft(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
owner, _ = service_with_fake_runtime(tmp_path)
|
|
request = _retired_fresh_reopen_request(
|
|
owner,
|
|
reopening_id="reopening-quick-restart-lost-response",
|
|
connection_mode="quick-connect",
|
|
)
|
|
owner.reopen_retired_physical_command_reconciliation(request)
|
|
|
|
restarted, restarted_runtime = service_with_fake_runtime(tmp_path)
|
|
assert restarted.state()["desired_connection_mode"] == "bridge"
|
|
monkeypatch.setattr(
|
|
restarted,
|
|
"_acquire_k1_lifecycle_process_lease",
|
|
lambda *_args, **_kwargs: (_ for _ in ()).throw(
|
|
AssertionError("durable reopen retry must not reacquire the lease")
|
|
),
|
|
)
|
|
|
|
observed = restarted.reopen_retired_physical_command_reconciliation(request)
|
|
after_release = restarted.state()
|
|
|
|
assert observed["desired_connection_mode"] == "bridge"
|
|
assert after_release["connection_lifecycle"]["mode_selection"]["allowed"] is True
|
|
assert [
|
|
item["reopening_id"]
|
|
for item in observed["physical_command"]["record"]["operator_reconciliation_reopens"]
|
|
] == ["reopening-quick-restart-lost-response"]
|
|
assert restarted_runtime.start_calls == []
|
|
assert restarted_runtime.stop_calls == 0
|
|
|
|
|
|
def test_reopen_cross_process_lifecycle_owner_blocks_before_ledger_commit(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
owner, _ = service_with_fake_runtime(tmp_path)
|
|
request = _retired_fresh_reopen_request(
|
|
owner,
|
|
reopening_id="reopening-cross-process-owner",
|
|
)
|
|
actor, actor_runtime = service_with_fake_runtime(tmp_path)
|
|
with actor._lock: # noqa: SLF001
|
|
actor._ble_discovery_generation = request.expected_discovery_generation # noqa: SLF001
|
|
_set_scanned_k1(actor, device_id=request.expected_transport_ref)
|
|
assert (
|
|
actor._physical_operator_reconciliation_reopen_projection()[ # noqa: SLF001
|
|
"allowed"
|
|
]
|
|
is False
|
|
)
|
|
with actor._lock: # noqa: SLF001
|
|
actor._ble_discovery_generation += 1 # noqa: SLF001
|
|
_set_scanned_k1(actor, device_id=request.expected_transport_ref)
|
|
actor_projection = actor._physical_operator_reconciliation_reopen_projection() # noqa: SLF001
|
|
assert actor_projection["allowed"] is True
|
|
actor_request = ReopenRetiredPhysicalCommandReconciliationRequest(
|
|
reopening_id=request.reopening_id,
|
|
expected_revision=actor_projection["expected_revision"],
|
|
expected_retirement_id=actor_projection["expected_retirement_id"],
|
|
expected_transport_ref=actor_projection["expected_transport_ref"],
|
|
expected_discovery_generation=actor_projection["expected_discovery_generation"],
|
|
expected_desired_mode=actor_projection["expected_desired_mode"],
|
|
expected_desired_mode_revision=actor_projection["expected_desired_mode_revision"],
|
|
operator_confirmed=True,
|
|
reason=request.reason,
|
|
)
|
|
before = actor._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
|
|
owner._acquire_k1_lifecycle_process_lease("control") # noqa: SLF001
|
|
try:
|
|
with pytest.raises(facade_module.ApplicationControlProcessLeaseUnavailable):
|
|
actor.reopen_retired_physical_command_reconciliation(actor_request)
|
|
finally:
|
|
owner._release_k1_lifecycle_process_lease("control") # noqa: SLF001
|
|
|
|
assert actor._physical_command_ledger.snapshot().record == before # noqa: SLF001
|
|
assert actor._application_control_process_lease_holders == set() # noqa: SLF001
|
|
assert actor_runtime.start_calls == []
|
|
assert actor_runtime.stop_calls == 0
|
|
|
|
|
|
def test_peer_requires_a_new_scan_after_observing_shared_physical_retirement(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
owner, owner_runtime = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(
|
|
owner,
|
|
transport_ref=DURABLE_K1_UUID,
|
|
connection_mode="bridge",
|
|
)
|
|
retirement = _seed_unresolved_physical_stop_for_retirement(owner, binding)
|
|
|
|
peer, peer_runtime = service_with_fake_runtime(tmp_path)
|
|
with peer._lock: # noqa: SLF001
|
|
peer._ble_discovery_generation += 1 # noqa: SLF001
|
|
stale_generation = peer._ble_discovery_generation # noqa: SLF001
|
|
_set_scanned_k1(peer, device_id=DURABLE_K1_UUID.lower())
|
|
|
|
owner.retire_unavailable_physical_command(retirement)
|
|
retired_before = peer._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
stale = peer._physical_operator_reconciliation_reopen_projection() # noqa: SLF001
|
|
|
|
assert stale["allowed"] is False
|
|
assert "physical-command-reconciliation-reopen-target-not-observed" in stale["reason_codes"]
|
|
assert ( # noqa: SLF001
|
|
peer._physical_retirement_reopen_generation_floors[retirement.retirement_id]
|
|
== stale_generation
|
|
)
|
|
assert peer._physical_command_ledger.snapshot().record == retired_before # noqa: SLF001
|
|
|
|
with peer._lock: # noqa: SLF001
|
|
peer._ble_discovery_generation += 1 # noqa: SLF001
|
|
_set_scanned_k1(peer, device_id=DURABLE_K1_UUID.lower())
|
|
fresh = peer._physical_operator_reconciliation_reopen_projection() # noqa: SLF001
|
|
|
|
assert fresh["allowed"] is True, fresh
|
|
reopened = peer.reopen_retired_physical_command_reconciliation(
|
|
ReopenRetiredPhysicalCommandReconciliationRequest(
|
|
reopening_id="reopening-after-peer-post-retirement-scan",
|
|
expected_revision=fresh["expected_revision"],
|
|
expected_retirement_id=fresh["expected_retirement_id"],
|
|
expected_transport_ref=fresh["expected_transport_ref"],
|
|
expected_discovery_generation=fresh["expected_discovery_generation"],
|
|
expected_desired_mode=fresh["expected_desired_mode"],
|
|
expected_desired_mode_revision=fresh["expected_desired_mode_revision"],
|
|
operator_confirmed=True,
|
|
reason="device-returned-for-explicit-reconciliation",
|
|
)
|
|
)
|
|
|
|
assert reopened["physical_command"]["requires_reconciliation"] is True
|
|
assert owner_runtime.start_calls == []
|
|
assert owner_runtime.stop_calls == 0
|
|
assert peer_runtime.start_calls == []
|
|
assert peer_runtime.stop_calls == 0
|
|
|
|
|
|
def test_exact_retirement_retry_repairs_local_revocation_without_process_lease_or_io(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(service, transport_ref="retired-k1")
|
|
request = _seed_unresolved_physical_stop_for_retirement(service, binding)
|
|
real_revoke = service._revoke_retired_physical_target_locally # noqa: SLF001
|
|
|
|
def fail_after_durable_commit(*_args: object, **_kwargs: object) -> None:
|
|
raise RuntimeError("injected crash after ledger commit")
|
|
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_revoke_retired_physical_target_locally",
|
|
fail_after_durable_commit,
|
|
)
|
|
with pytest.raises(RuntimeError, match="injected crash"):
|
|
service.retire_unavailable_physical_command(request)
|
|
committed = service._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
assert committed is not None
|
|
assert committed.resolution == "operator-retired-outcome-unknown"
|
|
assert service._selected_device_id == "retired-k1" # noqa: SLF001
|
|
|
|
# A replacement backend may legitimately advance the shared ledger before
|
|
# the original browser retries its lost retirement response. The carried
|
|
# retirement audit, not the current operation, is the idempotency key.
|
|
replacement_identity = PhysicalCommandIdentity(
|
|
vendor_device_id_sha256="7" * 64,
|
|
device_serial_sha256="8" * 64,
|
|
)
|
|
replacement_connection = PhysicalCommandConnectionBinding(
|
|
intent_id="replacement-intent",
|
|
transport_ref="replacement-k1",
|
|
connection_mode="bridge",
|
|
target_ipv4="192.168.1.21",
|
|
target_port=facade_module.CONTROL_MQTT_PORT,
|
|
host_path_epoch=21,
|
|
control_session_id="replacement-control-session",
|
|
producer_generation=22,
|
|
)
|
|
replacement_status = PhysicalCommandStatusEvidence(
|
|
source="live-control-session",
|
|
vendor_device_id_sha256=replacement_identity.vendor_device_id_sha256,
|
|
device_serial_sha256=replacement_identity.device_serial_sha256,
|
|
control_session_id=replacement_connection.control_session_id,
|
|
host_path_epoch=replacement_connection.host_path_epoch,
|
|
producer_generation=replacement_connection.producer_generation,
|
|
session_state="ready",
|
|
session_state_code=300,
|
|
project_bound=False,
|
|
project_id_sha256=None,
|
|
init_ready=False,
|
|
status_message_sha256="9" * 64,
|
|
mqtt_retained=False,
|
|
observed_at_utc="2026-08-10T17:01:00.000Z",
|
|
)
|
|
replacement = service._physical_command_ledger.prepare( # noqa: SLF001
|
|
operation_id="replacement-start-after-retirement",
|
|
parent_operation_id=committed.operation_id,
|
|
acquisition_id="replacement-acquisition-after-retirement",
|
|
action="start",
|
|
identity=replacement_identity,
|
|
connection=replacement_connection,
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
payload_sha256="2" * 64,
|
|
baseline_status=replacement_status,
|
|
)
|
|
advanced = service._physical_command_ledger.resolve( # noqa: SLF001
|
|
replacement.operation_id,
|
|
resolution="not-dispatched",
|
|
)
|
|
|
|
monkeypatch.setattr(service, "_revoke_retired_physical_target_locally", real_revoke)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_acquire_k1_lifecycle_process_lease",
|
|
lambda *_args, **_kwargs: (_ for _ in ()).throw(
|
|
AssertionError("exact retry must not acquire the shared process lease")
|
|
),
|
|
)
|
|
repaired = service.retire_unavailable_physical_command(request)
|
|
|
|
persisted = service._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
assert persisted == advanced
|
|
assert repaired["selected_device_id"] is None
|
|
assert repaired["connection_lifecycle"]["active_binding"] is None
|
|
|
|
|
|
def test_stale_peer_blocks_every_retired_target_contact_path_before_io(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
owner, _ = service_with_fake_runtime(tmp_path)
|
|
owner_binding = _seed_supervised_connection(owner, transport_ref="RETIRED-K1")
|
|
request = _seed_unresolved_physical_stop_for_retirement(owner, owner_binding)
|
|
stale_peer, stale_runtime = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(stale_peer, device_id="retired-k1")
|
|
_seed_supervised_connection(stale_peer, transport_ref="retired-k1")
|
|
connect_request = _connect_request(
|
|
device_id="retired-k1",
|
|
ssid="DCCONSTRUCTIONS",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
connection_mode="bridge",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
|
|
owner.retire_unavailable_physical_command(request)
|
|
io_calls: list[str] = []
|
|
|
|
async def forbidden_async_io(*_args: object, **_kwargs: object) -> object:
|
|
io_calls.append("ble-or-network")
|
|
raise AssertionError("retired target must be rejected before I/O")
|
|
|
|
def forbidden_sync_io(*_args: object, **_kwargs: object) -> object:
|
|
io_calls.append("host-tcp-mqtt-http")
|
|
raise AssertionError("retired target must be rejected before I/O")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_async_io)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_async_io)
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", forbidden_sync_io)
|
|
monkeypatch.setattr(stale_peer, "_sample_host_path", forbidden_sync_io)
|
|
monkeypatch.setattr(stale_peer, "_probe_control_endpoint", forbidden_sync_io)
|
|
monkeypatch.setattr(stale_peer, "_refresh_live_lan_address", forbidden_sync_io)
|
|
monkeypatch.setattr(stale_peer.camera_preview, "select", forbidden_sync_io)
|
|
monkeypatch.setattr(
|
|
stale_peer._calibration_snapshot_reader, # noqa: SLF001
|
|
"capture",
|
|
forbidden_sync_io,
|
|
)
|
|
monkeypatch.setattr(
|
|
SecretStr,
|
|
"get_secret_value",
|
|
lambda _self: (_ for _ in ()).throw(
|
|
AssertionError("retired Connect must not unwrap the credential")
|
|
),
|
|
)
|
|
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as connect_error:
|
|
asyncio.run(stale_peer.connect(connect_request))
|
|
assert connect_error.value.reason_code == "physical-command-target-retired"
|
|
with pytest.raises(facade_module.ConnectionVerificationError) as verify_error:
|
|
asyncio.run(
|
|
stale_peer.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="retired-k1",
|
|
source="fresh-scan",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_discovery_generation=0,
|
|
)
|
|
)
|
|
)
|
|
assert verify_error.value.reason_code == "physical-command-target-retired"
|
|
with pytest.raises(facade_module.ConnectionVerificationError) as open_error:
|
|
stale_peer.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 open_error.value.reason_code == "physical-command-target-retired"
|
|
with pytest.raises(facade_module.ConfiguredEndpointProbeError) as probe_error:
|
|
asyncio.run(stale_peer.probe_configured_endpoint(ConfiguredEndpointProbeRequest()))
|
|
assert probe_error.value.reason_code == "physical-command-target-retired"
|
|
with pytest.raises(facade_module.ConnectionVerificationError) as calibration_error:
|
|
stale_peer.read_device_calibration_snapshot()
|
|
assert calibration_error.value.reason_code == "physical-command-target-retired"
|
|
assert stale_peer._connection_monitor_target() is None # noqa: SLF001
|
|
asyncio.run(stale_peer._connection_monitor.poll_once()) # noqa: SLF001
|
|
with pytest.raises(facade_module.ConnectionVerificationError):
|
|
asyncio.run(
|
|
stale_peer._monitor_control_endpoint( # noqa: SLF001
|
|
EndpointTarget("192.168.1.20", facade_module.CONTROL_MQTT_PORT)
|
|
)
|
|
)
|
|
with pytest.raises(facade_module.ConnectionVerificationError):
|
|
stale_peer.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="RETIRED01",
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
with pytest.raises(facade_module.ConnectionVerificationError):
|
|
stale_peer.start_live(
|
|
"RETIRED02",
|
|
"192.168.1.20",
|
|
None,
|
|
ATTESTATION,
|
|
)
|
|
with pytest.raises(facade_module.ConnectionVerificationError) as camera_error:
|
|
stale_peer.select_camera_preview(
|
|
CameraPreviewSelectRequest(
|
|
source_id=facade_module.DEFAULT_ACQUISITION_CAMERA_SOURCE,
|
|
device_session_id="test-session-retired-k1",
|
|
)
|
|
)
|
|
assert camera_error.value.reason_code == "physical-command-target-retired"
|
|
|
|
inspected = stale_peer.inspect_device()
|
|
assert inspected["physical_command"]["record"]["resolution"] == (
|
|
"operator-retired-outcome-unknown"
|
|
)
|
|
assert stale_runtime.start_calls == []
|
|
assert stale_runtime.stop_calls == 0
|
|
assert io_calls == []
|
|
|
|
|
|
def test_selected_camera_holds_retirement_fence_and_stale_generation_cannot_spawn(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
camera_service, _ = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(
|
|
camera_service,
|
|
transport_ref="retired-k1",
|
|
)
|
|
request = _seed_unresolved_physical_stop_for_retirement(
|
|
camera_service,
|
|
binding,
|
|
)
|
|
camera_service.camera_preview._ffmpeg_path = ( # noqa: SLF001
|
|
tmp_path / "synthetic-ffmpeg"
|
|
)
|
|
|
|
selected = camera_service.select_camera_preview(
|
|
CameraPreviewSelectRequest(
|
|
source_id=facade_module.DEFAULT_ACQUISITION_CAMERA_SOURCE,
|
|
device_session_id="test-session-retired-k1",
|
|
)
|
|
)
|
|
generation = selected["camera_preview"]["generation"]
|
|
assert isinstance(generation, int)
|
|
assert "camera" in ( # noqa: SLF001
|
|
camera_service._application_control_process_lease_holders # noqa: SLF001
|
|
)
|
|
|
|
retirement_service, _ = service_with_fake_runtime(tmp_path)
|
|
with pytest.raises(facade_module.ApplicationControlProcessLeaseUnavailable):
|
|
retirement_service.retire_unavailable_physical_command(request)
|
|
|
|
stopped = camera_service.stop_camera_preview(
|
|
facade_module.CameraPreviewStopRequest(
|
|
generation=generation,
|
|
device_session_id="test-session-retired-k1",
|
|
)
|
|
)
|
|
assert stopped["camera_preview"]["active_source_id"] is None
|
|
assert "camera" not in ( # noqa: SLF001
|
|
camera_service._application_control_process_lease_holders # noqa: SLF001
|
|
)
|
|
|
|
retired = retirement_service.retire_unavailable_physical_command(request)
|
|
assert retired["physical_command"]["record"]["resolution"] == (
|
|
"operator-retired-outcome-unknown"
|
|
)
|
|
|
|
popen_calls: list[str] = []
|
|
|
|
def forbidden_popen(*_args: object, **_kwargs: object) -> object:
|
|
popen_calls.append("popen")
|
|
raise AssertionError("retired stale camera generation must not spawn FFmpeg")
|
|
|
|
monkeypatch.setattr(
|
|
"k1link.device_plugins.xgrids_k1.camera.subprocess.Popen",
|
|
forbidden_popen,
|
|
)
|
|
with pytest.raises(ValueError, match="generation не активно"):
|
|
camera_service.camera_preview.open_delivery(generation)
|
|
assert popen_calls == []
|
|
|
|
|
|
def test_cancelled_target_probe_retains_retirement_fence_until_thread_finishes(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
probe_service, _ = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(
|
|
probe_service,
|
|
transport_ref="retired-k1",
|
|
)
|
|
request = _seed_unresolved_physical_stop_for_retirement(
|
|
probe_service,
|
|
binding,
|
|
)
|
|
retirement_service, _ = service_with_fake_runtime(tmp_path)
|
|
probe_entered = threading.Event()
|
|
release_probe = threading.Event()
|
|
|
|
def blocked_host_path(
|
|
_target: str,
|
|
*,
|
|
association_timeout_seconds: float = 30.0,
|
|
fail_closed_unproven_association: bool = False,
|
|
) -> HostPathProbeResult:
|
|
assert association_timeout_seconds == (
|
|
facade_module.CONNECTION_MONITOR_ASSOCIATION_TIMEOUT_SECONDS
|
|
)
|
|
assert fail_closed_unproven_association is True
|
|
probe_entered.set()
|
|
assert release_probe.wait(timeout=5.0)
|
|
return _direct_host_path(_target)
|
|
|
|
monkeypatch.setattr(probe_service, "_sample_host_path", blocked_host_path)
|
|
|
|
async def scenario() -> None:
|
|
target = EndpointTarget(binding.target_ipv4, binding.target_port)
|
|
probe_task = asyncio.create_task(
|
|
probe_service._monitor_host_path(target) # noqa: SLF001
|
|
)
|
|
assert await asyncio.to_thread(probe_entered.wait, 5.0)
|
|
before = retirement_service._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
|
|
probe_task.cancel()
|
|
await asyncio.sleep(0)
|
|
probe_task.cancel()
|
|
await asyncio.sleep(0.01)
|
|
assert probe_task.done() is False
|
|
with pytest.raises(facade_module.ApplicationControlProcessLeaseUnavailable):
|
|
await asyncio.to_thread(
|
|
retirement_service.retire_unavailable_physical_command,
|
|
request,
|
|
)
|
|
assert (
|
|
retirement_service._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
== before
|
|
)
|
|
|
|
release_probe.set()
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await probe_task
|
|
retired = await asyncio.to_thread(
|
|
retirement_service.retire_unavailable_physical_command,
|
|
request,
|
|
)
|
|
assert retired["physical_command"]["record"]["resolution"] == (
|
|
"operator-retired-outcome-unknown"
|
|
)
|
|
|
|
asyncio.run(scenario())
|
|
|
|
|
|
def test_calibration_process_lease_blocks_cross_service_retirement_until_capture_finishes(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
calibration_service, _ = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(
|
|
calibration_service,
|
|
transport_ref="retired-k1",
|
|
)
|
|
request = _seed_unresolved_physical_stop_for_retirement(
|
|
calibration_service,
|
|
binding,
|
|
)
|
|
retirement_service, _ = service_with_fake_runtime(tmp_path)
|
|
capture_entered = threading.Event()
|
|
release_capture = threading.Event()
|
|
outcome: dict[str, object] = {}
|
|
|
|
class BlockingCalibrationReader:
|
|
def capture(self, **_kwargs: object) -> dict[str, object]:
|
|
capture_entered.set()
|
|
assert release_capture.wait(timeout=5)
|
|
return {"status": "available", "snapshot_id": "blocking-test"}
|
|
|
|
calibration_service._calibration_snapshot_reader = ( # type: ignore[assignment] # noqa: SLF001
|
|
BlockingCalibrationReader()
|
|
)
|
|
monkeypatch.setattr(
|
|
calibration_service,
|
|
"_refresh_live_lan_address",
|
|
lambda: "192.168.1.20",
|
|
)
|
|
|
|
def capture() -> None:
|
|
try:
|
|
outcome["result"] = calibration_service.read_device_calibration_snapshot()
|
|
except BaseException as exc: # pragma: no cover - asserted below
|
|
outcome["error"] = exc
|
|
|
|
worker = threading.Thread(target=capture, daemon=True)
|
|
worker.start()
|
|
assert capture_entered.wait(timeout=5)
|
|
projection = calibration_service.state()["physical_command"]["operator_retirement"]
|
|
assert projection["allowed"] is False
|
|
assert "device-calibration-read-active" in projection["reason_codes"]
|
|
before = retirement_service._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
with pytest.raises(facade_module.ApplicationControlProcessLeaseUnavailable):
|
|
retirement_service.retire_unavailable_physical_command(request)
|
|
unchanged = retirement_service._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
assert unchanged == before
|
|
|
|
release_capture.set()
|
|
worker.join(timeout=5)
|
|
assert not worker.is_alive()
|
|
assert "error" not in outcome
|
|
assert outcome["result"] == {
|
|
"status": "available",
|
|
"snapshot_id": "blocking-test",
|
|
}
|
|
|
|
|
|
def test_calibration_hard_timeout_retains_retirement_fence_until_ble_cleanup(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
calibration_service, _ = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(
|
|
calibration_service,
|
|
transport_ref="retired-k1",
|
|
)
|
|
request = _seed_unresolved_physical_stop_for_retirement(
|
|
calibration_service,
|
|
binding,
|
|
)
|
|
retirement_service, _ = service_with_fake_runtime(tmp_path)
|
|
|
|
async def scenario() -> None:
|
|
cleanup_entered = asyncio.Event()
|
|
cleanup_release = asyncio.Event()
|
|
|
|
async def detached_status_read(
|
|
_device_id: str,
|
|
**_kwargs: object,
|
|
) -> dict[str, Any]:
|
|
async def stubborn_cleanup(_progress: BleOperationProgress) -> None:
|
|
try:
|
|
await asyncio.Event().wait()
|
|
except asyncio.CancelledError:
|
|
cleanup_entered.set()
|
|
await cleanup_release.wait()
|
|
raise
|
|
|
|
return await run_ble_operation( # type: ignore[return-value]
|
|
"status-read",
|
|
hard_timeout_seconds=0.01,
|
|
operation=stubborn_cleanup,
|
|
)
|
|
|
|
runtime_loop = asyncio.get_running_loop()
|
|
bind_ble_runtime_owner_loop(runtime_loop)
|
|
with calibration_service._lock: # noqa: SLF001
|
|
calibration_service._runtime_event_loop = runtime_loop # noqa: SLF001
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"read_wifi_status_once",
|
|
detached_status_read,
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_host_route_class",
|
|
lambda _target: "default-route",
|
|
)
|
|
|
|
with pytest.raises(RuntimeError, match="BLE status-read не завершился"):
|
|
await asyncio.to_thread(calibration_service.read_device_calibration_snapshot)
|
|
await cleanup_entered.wait()
|
|
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict):
|
|
await asyncio.to_thread(
|
|
retirement_service.retire_unavailable_physical_command,
|
|
request,
|
|
)
|
|
|
|
cleanup_release.set()
|
|
assert await wait_for_ble_runtime_idle()
|
|
retired = await asyncio.to_thread(
|
|
retirement_service.retire_unavailable_physical_command,
|
|
request,
|
|
)
|
|
assert retired["physical_command"]["record"]["resolution"] == (
|
|
"operator-retired-outcome-unknown"
|
|
)
|
|
|
|
asyncio.run(scenario())
|
|
|
|
|
|
def test_operator_manual_live_retains_cross_process_fence_until_receiver_stops(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
live_service, live_runtime = service_with_fake_runtime(tmp_path)
|
|
prepared = live_service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="MANUAL01",
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
|
|
retirement_service, _ = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(
|
|
retirement_service,
|
|
transport_ref="retired-k1",
|
|
)
|
|
request = _seed_unresolved_physical_stop_for_retirement(
|
|
retirement_service,
|
|
binding,
|
|
)
|
|
started = live_service.start_acquisition(_start_request(acquisition_id=acquisition_id))
|
|
|
|
assert started["acquisition"]["state"] == "starting"
|
|
assert live_runtime.start_calls
|
|
assert (
|
|
live_service._operator_manual_acquisition_process_lease_id # noqa: SLF001
|
|
== acquisition_id
|
|
)
|
|
with pytest.raises(facade_module.ApplicationControlProcessLeaseUnavailable):
|
|
retirement_service.retire_unavailable_physical_command(request)
|
|
unresolved = retirement_service._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
assert unresolved is not None and unresolved.unresolved
|
|
|
|
stopped = live_service.stop_acquisition(
|
|
_stop_request(
|
|
acquisition_id=acquisition_id,
|
|
mode="capture-only",
|
|
)
|
|
)
|
|
assert stopped["acquisition"]["state"] == "completed"
|
|
assert live_service._operator_manual_acquisition_process_lease_id is None # noqa: SLF001
|
|
|
|
retired = retirement_service.retire_unavailable_physical_command(request)
|
|
assert retired["physical_command"]["record"]["resolution"] == (
|
|
"operator-retired-outcome-unknown"
|
|
)
|
|
|
|
|
|
def test_ambiguous_manual_start_cleanup_keeps_retirement_fenced_until_retry(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
live_service, live_runtime = service_with_fake_runtime(tmp_path)
|
|
prepared = live_service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="MANUALFAIL",
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
retirement_service, _ = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(
|
|
retirement_service,
|
|
transport_ref="retired-k1",
|
|
)
|
|
request = _seed_unresolved_physical_stop_for_retirement(
|
|
retirement_service,
|
|
binding,
|
|
)
|
|
original_start_live = live_runtime.start_live
|
|
|
|
def fail_after_receiver_start(
|
|
host: str,
|
|
out_dir: Path,
|
|
*,
|
|
duration_seconds: float | None,
|
|
project_name: str,
|
|
recover_connection: Callable[[int], str] | None = None,
|
|
) -> None:
|
|
original_start_live(
|
|
host,
|
|
out_dir,
|
|
duration_seconds=duration_seconds,
|
|
project_name=project_name,
|
|
recover_connection=recover_connection,
|
|
)
|
|
raise RuntimeError("injected receiver start failure")
|
|
|
|
monkeypatch.setattr(live_runtime, "start_live", fail_after_receiver_start)
|
|
live_runtime.stop_error = RuntimeError("injected receiver cleanup failure")
|
|
camera_arm_calls: list[Path] = []
|
|
monkeypatch.setattr(
|
|
live_service,
|
|
"_arm_camera_recording",
|
|
lambda out_dir, **_kwargs: camera_arm_calls.append(out_dir),
|
|
)
|
|
|
|
with pytest.raises(RuntimeError, match="receiver start failure"):
|
|
live_service.start_acquisition(_start_request(acquisition_id=acquisition_id))
|
|
|
|
assert live_runtime.start_calls
|
|
assert camera_arm_calls == []
|
|
assert (
|
|
live_service._operator_manual_acquisition_process_lease_id # noqa: SLF001
|
|
== acquisition_id
|
|
)
|
|
with pytest.raises(facade_module.ApplicationControlProcessLeaseUnavailable):
|
|
retirement_service.retire_unavailable_physical_command(request)
|
|
unresolved = retirement_service._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
assert unresolved is not None and unresolved.unresolved
|
|
|
|
live_runtime.stop_error = None
|
|
live_service._stop_acquisition_sources( # noqa: SLF001
|
|
camera_status="failed",
|
|
camera_failure_code="cleanup-retry-after-start-failure",
|
|
)
|
|
assert live_service._operator_manual_acquisition_process_lease_id is None # noqa: SLF001
|
|
retirement_service.retire_unavailable_physical_command(request)
|
|
|
|
|
|
def test_state_snapshot_stamp_is_runtime_scoped_and_monotonic(tmp_path: Path) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path / "first-runtime")
|
|
|
|
first = service.state()
|
|
second = service.state()
|
|
replacement, _ = service_with_fake_runtime(tmp_path / "replacement-runtime")
|
|
replacement_state = replacement.state()
|
|
|
|
assert first["snapshot_runtime_id"].startswith("snapshot-runtime-")
|
|
assert first["snapshot_runtime_id"] == second["snapshot_runtime_id"]
|
|
assert first["snapshot_runtime_started_at_utc"] == (second["snapshot_runtime_started_at_utc"])
|
|
assert (
|
|
first["snapshot_runtime_started_monotonic_ns"]
|
|
== (second["snapshot_runtime_started_monotonic_ns"])
|
|
)
|
|
assert second["snapshot_revision"] == first["snapshot_revision"] + 1
|
|
assert replacement_state["snapshot_runtime_id"] != first["snapshot_runtime_id"]
|
|
assert int(replacement_state["snapshot_runtime_started_monotonic_ns"]) > int(
|
|
first["snapshot_runtime_started_monotonic_ns"]
|
|
)
|
|
assert replacement_state["snapshot_revision"] == 1
|
|
|
|
|
|
def test_runtime_start_allocator_is_strictly_increasing_for_equal_clock_ticks(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
with facade_module._SNAPSHOT_RUNTIME_START_LOCK: # noqa: SLF001
|
|
previous = facade_module._LAST_SNAPSHOT_RUNTIME_STARTED_MONOTONIC_NS # noqa: SLF001
|
|
facade_module._LAST_SNAPSHOT_RUNTIME_STARTED_MONOTONIC_NS = 100 # noqa: SLF001
|
|
try:
|
|
monkeypatch.setattr(facade_module.time, "monotonic_ns", lambda: 100)
|
|
first = facade_module._allocate_snapshot_runtime_started_monotonic_ns() # noqa: SLF001
|
|
second = facade_module._allocate_snapshot_runtime_started_monotonic_ns() # noqa: SLF001
|
|
assert (first, second) == ("101", "102")
|
|
finally:
|
|
with facade_module._SNAPSHOT_RUNTIME_START_LOCK: # noqa: SLF001
|
|
facade_module._LAST_SNAPSHOT_RUNTIME_STARTED_MONOTONIC_NS = max( # noqa: SLF001
|
|
previous,
|
|
102,
|
|
)
|
|
|
|
|
|
def _wifi_status_read(
|
|
ipv4: str | None,
|
|
*,
|
|
device_id: str = "test-ble-transport",
|
|
) -> 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": device_id,
|
|
"device_name": "XGR-K1",
|
|
"service_uuid": "00007f00-0000-1000-8000-00805f9b34fb",
|
|
"write_characteristic_uuid": "00007f01-0000-1000-8000-00805f9b34fb",
|
|
"write_characteristic_properties": ["read", "write"],
|
|
"max_write_without_response_size": 253,
|
|
"mtu_size": 256,
|
|
"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 _durable_status_capture(
|
|
device_id: str = DURABLE_K1_UUID,
|
|
) -> facade_module.CapturedDiscoveredDevice:
|
|
return facade_module.CapturedDiscoveredDevice(
|
|
device=SimpleNamespace(address=device_id), # type: ignore[arg-type]
|
|
macos_uuid=device_id,
|
|
owner_epoch=7,
|
|
source="retrieved-durable",
|
|
)
|
|
|
|
|
|
def _ap_ready_wifi_status() -> dict[str, Any]:
|
|
return {
|
|
"value_length": 54,
|
|
"mode": "WIFI_AP",
|
|
"ipv4": "192.168.56.1",
|
|
"status_code": 1,
|
|
"reserved": 1,
|
|
"trailer_hex": "",
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize("mode", [None, "UNKNOWN", "WIFI_AP"])
|
|
def test_applied_bridge_target_requires_explicit_wifi_client_mode(
|
|
mode: str | None,
|
|
) -> None:
|
|
status = {
|
|
"mode": mode,
|
|
"ipv4": "192.168.68.52",
|
|
"status_code": 1,
|
|
"reserved": 0,
|
|
}
|
|
|
|
assert facade_module._applied_network_target("bridge", status) is None # noqa: SLF001
|
|
assert (
|
|
facade_module._applied_network_target("direct-connect", status) # noqa: SLF001
|
|
is None
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("connection_mode", "baseline", "observed", "previous", "expected"),
|
|
[
|
|
(
|
|
"bridge",
|
|
NetworkStatusEvidence("WIFI_CLIENT", "192.168.68.40", 1, 0),
|
|
{"mode": "WIFI_CLIENT", "ipv4": "192.168.68.40", "status_code": 1, "reserved": 0},
|
|
None,
|
|
None,
|
|
),
|
|
(
|
|
"bridge",
|
|
NetworkStatusEvidence("WIFI_CLIENT", "192.168.68.40", 1, 0),
|
|
{"mode": "WIFI_CLIENT", "ipv4": "192.168.68.99", "status_code": 1, "reserved": 0},
|
|
None,
|
|
None,
|
|
),
|
|
(
|
|
"direct-connect",
|
|
NetworkStatusEvidence("WIFI_CLIENT", None, 1, 0),
|
|
{"mode": "WIFI_CLIENT", "ipv4": "172.20.10.2", "status_code": 1, "reserved": 0},
|
|
None,
|
|
None,
|
|
),
|
|
(
|
|
"quick-connect",
|
|
NetworkStatusEvidence("WIFI_AP", "192.168.56.1", 1, 1),
|
|
{"mode": "WIFI_AP", "ipv4": "192.168.56.1", "status_code": 2, "reserved": 1},
|
|
None,
|
|
None,
|
|
),
|
|
(
|
|
"bridge",
|
|
NetworkStatusEvidence("WIFI_AP", "192.168.56.1", 1, 1),
|
|
{"mode": "WIFI_CLIENT", "ipv4": "192.168.68.50", "status_code": 1, "reserved": 0},
|
|
None,
|
|
"192.168.68.50",
|
|
),
|
|
(
|
|
"direct-connect",
|
|
NetworkStatusEvidence("WIFI_AP", "192.168.56.1", 1, 1),
|
|
{"mode": "WIFI_CLIENT", "ipv4": "172.20.10.2", "status_code": 1, "reserved": 0},
|
|
None,
|
|
"172.20.10.2",
|
|
),
|
|
(
|
|
"quick-connect",
|
|
NetworkStatusEvidence("WIFI_CLIENT", "192.168.68.50", 1, 0),
|
|
{"mode": "WIFI_AP", "ipv4": "192.168.56.1", "status_code": 1, "reserved": 1},
|
|
None,
|
|
"192.168.56.1",
|
|
),
|
|
(
|
|
"quick-connect",
|
|
NetworkStatusEvidence("WIFI_AP", "192.168.56.1", 1, 0),
|
|
{"mode": "WIFI_AP", "ipv4": "192.168.56.1", "status_code": 1, "reserved": 1},
|
|
None,
|
|
"192.168.56.1",
|
|
),
|
|
(
|
|
"quick-connect",
|
|
NetworkStatusEvidence("WIFI_CLIENT", "192.168.68.50", 1, 0),
|
|
{"mode": "WIFI_AP", "ipv4": "192.168.56.1", "status_code": 1, "reserved": 1},
|
|
PreviousConnectionEvidence("same-k1", "quick-connect", "192.168.56.1", "old-session"),
|
|
None,
|
|
),
|
|
(
|
|
"bridge",
|
|
NetworkStatusEvidence("WIFI_AP", "192.168.56.1", 1, 1),
|
|
{"mode": "WIFI_CLIENT", "ipv4": "192.168.68.99", "status_code": 1, "reserved": 0},
|
|
PreviousConnectionEvidence("same-k1", "bridge", "192.168.68.40", "old-session"),
|
|
None,
|
|
),
|
|
(
|
|
"direct-connect",
|
|
NetworkStatusEvidence("WIFI_AP", "192.168.56.1", 1, 1),
|
|
{"mode": "WIFI_CLIENT", "ipv4": "172.20.10.2", "status_code": 1, "reserved": 0},
|
|
PreviousConnectionEvidence("same-k1", "bridge", "172.20.10.2", "old-session"),
|
|
None,
|
|
),
|
|
(
|
|
"bridge",
|
|
NetworkStatusEvidence("WIFI_AP", "192.168.56.1", 1, 1),
|
|
{"mode": "WIFI_CLIENT", "ipv4": "172.20.10.2", "status_code": 1, "reserved": 0},
|
|
PreviousConnectionEvidence("same-k1", "direct-connect", "172.20.10.2", "old-session"),
|
|
None,
|
|
),
|
|
(
|
|
"bridge",
|
|
NetworkStatusEvidence("WIFI_AP", "192.168.56.1", 1, 1),
|
|
{"mode": "WIFI_CLIENT", "ipv4": "192.168.68.50", "status_code": 1, "reserved": 0},
|
|
PreviousConnectionEvidence("other-k1", "bridge", "192.168.68.50", "old-session"),
|
|
"192.168.68.50",
|
|
),
|
|
],
|
|
)
|
|
def test_post_dispatch_target_requires_status_distinguishable_from_baseline(
|
|
connection_mode: facade_module.ConnectionMode,
|
|
baseline: NetworkStatusEvidence,
|
|
observed: dict[str, object],
|
|
previous: PreviousConnectionEvidence | None,
|
|
expected: str | None,
|
|
) -> None:
|
|
assert (
|
|
facade_module._post_dispatch_network_target( # noqa: SLF001
|
|
connection_mode,
|
|
observed,
|
|
transport_ref="same-k1",
|
|
baseline_status=baseline,
|
|
previous_connection=previous,
|
|
)
|
|
== expected
|
|
)
|
|
|
|
|
|
def test_post_dispatch_bridge_accepts_exact_fw302_network_name_when_baseline_is_same() -> None:
|
|
"""An idempotent same-network Bridge request proves the requested target."""
|
|
|
|
status = {
|
|
"mode": "WIFI_CLIENT",
|
|
"network_name": "LAB_NETWORK",
|
|
"ipv4": "192.168.68.51",
|
|
"status_code": 1,
|
|
"reserved": 0,
|
|
}
|
|
|
|
assert (
|
|
facade_module._post_dispatch_network_target( # noqa: SLF001
|
|
"bridge",
|
|
status,
|
|
transport_ref="same-k1",
|
|
baseline_status=NetworkStatusEvidence("WIFI_CLIENT", "192.168.68.51", 1, 0),
|
|
previous_connection=PreviousConnectionEvidence(
|
|
"same-k1",
|
|
"bridge",
|
|
"192.168.68.51",
|
|
"old-session",
|
|
),
|
|
expected_network_name="LAB_NETWORK",
|
|
)
|
|
== "192.168.68.51"
|
|
)
|
|
|
|
|
|
def test_post_dispatch_bridge_rejects_another_fw302_network_name() -> None:
|
|
status = {
|
|
"mode": "WIFI_CLIENT",
|
|
"network_name": "ANOTHER_NETWORK",
|
|
"ipv4": "192.168.68.51",
|
|
"status_code": 1,
|
|
"reserved": 0,
|
|
}
|
|
|
|
assert (
|
|
facade_module._post_dispatch_network_target( # noqa: SLF001
|
|
"bridge",
|
|
status,
|
|
transport_ref="same-k1",
|
|
baseline_status=NetworkStatusEvidence("WIFI_AP", "192.168.56.1", 1, 1),
|
|
expected_network_name="LAB_NETWORK",
|
|
)
|
|
is None
|
|
)
|
|
|
|
|
|
def _seed_unresolved_network_mutation(
|
|
service: XgridsK1CompatibilityService,
|
|
*,
|
|
operation_id: str = "ambiguous-operation",
|
|
idempotency_operation_id: str | None = None,
|
|
transport_ref: str = "test-ble-transport",
|
|
intended_mode: NetworkConnectionMode = "bridge",
|
|
baseline_status: NetworkStatusEvidence | None = None,
|
|
previous_connection: PreviousConnectionEvidence | None = None,
|
|
) -> dict[str, Any]:
|
|
resolved_idempotency_operation_id = idempotency_operation_id or operation_id
|
|
idempotency_key = f"test-reconciliation-{resolved_idempotency_operation_id}"
|
|
idempotency_journal = (
|
|
service._require_network_provisioning_idempotency_journal() # noqa: SLF001
|
|
)
|
|
idempotency_admission = idempotency_journal.begin(
|
|
idempotency_key=idempotency_key,
|
|
action=facade_module.ACTION_NETWORK_PROVISION,
|
|
operation_id=resolved_idempotency_operation_id,
|
|
request_binding_sha256=facade_module.derive_request_binding_sha256(
|
|
idempotency_key,
|
|
action=facade_module.ACTION_NETWORK_PROVISION,
|
|
canonical_request=b'{"test":"read-only-reconciliation"}',
|
|
),
|
|
)
|
|
ledger = service._network_mutation_ledger # noqa: SLF001
|
|
prepared = ledger.prepare(
|
|
operation_id=operation_id,
|
|
transport_ref=transport_ref,
|
|
intended_mode=intended_mode,
|
|
write_mode="with_response",
|
|
baseline_status=baseline_status
|
|
or NetworkStatusEvidence(
|
|
mode="WIFI_AP",
|
|
ipv4="192.168.56.1",
|
|
status_code=1,
|
|
reserved=1,
|
|
),
|
|
previous_connection=previous_connection,
|
|
)
|
|
idempotency_journal.mark_unresolved(
|
|
resolved_idempotency_operation_id,
|
|
expected_revision=idempotency_admission.record.revision,
|
|
)
|
|
ledger.mark_dispatching(
|
|
operation_id,
|
|
expected_revision=prepared.revision,
|
|
)
|
|
fence = service.state()["network_write_reconciliation"]
|
|
assert fence is not None
|
|
return fence
|
|
|
|
|
|
def _prepare_network_mutation_for_reopen(
|
|
service: XgridsK1CompatibilityService,
|
|
*,
|
|
operation_id: str,
|
|
transport_ref: str,
|
|
intended_mode: NetworkConnectionMode,
|
|
) -> NetworkMutationRecord:
|
|
return service._network_mutation_ledger.prepare( # noqa: SLF001
|
|
operation_id=operation_id,
|
|
transport_ref=transport_ref,
|
|
intended_mode=intended_mode,
|
|
write_mode="with_response",
|
|
baseline_status=NetworkStatusEvidence(
|
|
mode="WIFI_AP",
|
|
ipv4="192.168.56.1",
|
|
status_code=1,
|
|
reserved=1,
|
|
),
|
|
)
|
|
|
|
|
|
def _begin_network_idempotency_for_reopen(
|
|
service: XgridsK1CompatibilityService,
|
|
*,
|
|
operation_id: str,
|
|
mark_unresolved: bool,
|
|
) -> None:
|
|
idempotency_key = f"reopen-audit-{operation_id}"
|
|
journal = service._require_network_provisioning_idempotency_journal() # noqa: SLF001
|
|
admission = journal.begin(
|
|
idempotency_key=idempotency_key,
|
|
action=facade_module.ACTION_NETWORK_PROVISION,
|
|
operation_id=operation_id,
|
|
request_binding_sha256=facade_module.derive_request_binding_sha256(
|
|
idempotency_key,
|
|
action=facade_module.ACTION_NETWORK_PROVISION,
|
|
canonical_request=b'{"test":"reopen-precommit"}',
|
|
),
|
|
)
|
|
if mark_unresolved:
|
|
journal.mark_unresolved(
|
|
operation_id,
|
|
expected_revision=admission.record.revision,
|
|
)
|
|
|
|
|
|
def _seed_legacy_network_mutation_without_idempotency(
|
|
service: XgridsK1CompatibilityService,
|
|
*,
|
|
operation_id: str = "legacy-network-operation",
|
|
stage: str = "dispatching",
|
|
) -> None:
|
|
"""Create the durable shape emitted before the idempotency journal existed."""
|
|
|
|
ledger = service._network_mutation_ledger # noqa: SLF001
|
|
prepared = ledger.prepare(
|
|
operation_id=operation_id,
|
|
transport_ref="test-ble-transport",
|
|
intended_mode="bridge",
|
|
write_mode="with_response",
|
|
baseline_status=NetworkStatusEvidence(
|
|
mode="WIFI_AP",
|
|
ipv4="192.168.56.1",
|
|
status_code=1,
|
|
reserved=1,
|
|
),
|
|
)
|
|
if stage == "prepared":
|
|
return
|
|
dispatching = ledger.mark_dispatching(
|
|
operation_id,
|
|
expected_revision=prepared.revision,
|
|
)
|
|
if stage == "observing":
|
|
ledger.mark_observing(
|
|
operation_id,
|
|
expected_revision=dispatching.revision,
|
|
write_confirmed=True,
|
|
observation=NetworkStatusEvidence(
|
|
mode="WIFI_CLIENT",
|
|
ipv4="192.168.68.50",
|
|
status_code=1,
|
|
reserved=0,
|
|
),
|
|
)
|
|
elif stage != "dispatching":
|
|
raise ValueError("unsupported legacy test stage")
|
|
|
|
|
|
def _set_scanned_devices(
|
|
service: XgridsK1CompatibilityService,
|
|
devices: list[dict[str, Any]],
|
|
) -> None:
|
|
service._devices = devices # noqa: SLF001
|
|
_SYNTHETIC_SCAN_CAPTURES.clear()
|
|
_SYNTHETIC_SCAN_CAPTURES.update(
|
|
{
|
|
str(item["device_id"]): facade_module.CapturedDiscoveredDevice(
|
|
device=SimpleNamespace(address=str(item["device_id"])), # type: ignore[arg-type]
|
|
macos_uuid=str(item["device_id"]),
|
|
owner_epoch=1,
|
|
scan_generation=service._ble_discovery_generation, # noqa: SLF001
|
|
)
|
|
for item in devices
|
|
}
|
|
)
|
|
observed_monotonic = facade_module.time.monotonic()
|
|
observed_suspend_aware = facade_module.time.time()
|
|
service._ble_device_last_seen_monotonic = { # noqa: SLF001
|
|
str(item["device_id"]): observed_monotonic for item in devices
|
|
}
|
|
service._ble_device_last_seen_suspend_aware = { # noqa: SLF001
|
|
str(item["device_id"]): observed_suspend_aware 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 _ble_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,
|
|
}
|
|
]
|
|
}
|
|
|
|
|
|
def _resolved_active_physical_snapshot(
|
|
*,
|
|
transport_ref: str = "k1-original",
|
|
connection_mode: str = "bridge",
|
|
) -> dict[str, object]:
|
|
return {
|
|
"status": "resolved",
|
|
"reason_code": None,
|
|
"requires_reconciliation": False,
|
|
"resolved_active_recovery_required": True,
|
|
"automatic_replay_allowed": False,
|
|
"normal_session_recovery_supported": False,
|
|
"recovery_requirement": ("explicit-read-only-deviceinfo-and-non-retained-devicestatus"),
|
|
"runtime_bound": False,
|
|
"reconciliation_ready": False,
|
|
"observed_session_state": None,
|
|
"active_operation_id": None,
|
|
"record": {
|
|
"operation_id": "persisted-start-active",
|
|
"action": "start",
|
|
"stage": "resolved",
|
|
"resolution": "start-active-observed",
|
|
"connection": {
|
|
"transport_ref": transport_ref,
|
|
"connection_mode": connection_mode,
|
|
},
|
|
"reconciliations": [],
|
|
},
|
|
}
|
|
|
|
|
|
def _pending_reopened_physical_snapshot(
|
|
*,
|
|
transport_ref: str = "k1-a",
|
|
connection_mode: str = "bridge",
|
|
) -> dict[str, object]:
|
|
"""Project a classified STOP whose retired target was reopened for Verify."""
|
|
|
|
operation_id = "pending-reopened-prepared-stop"
|
|
retirement_id = "retirement-before-pending-reopen"
|
|
return {
|
|
"status": "resolved",
|
|
"reason_code": "physical-command-reconciliation-required",
|
|
"requires_reconciliation": False,
|
|
"resolved_active_recovery_required": True,
|
|
"reopened_physical_state_recovery_required": True,
|
|
"physical_active": False,
|
|
"reconciled_physical_state": None,
|
|
"automatic_replay_allowed": False,
|
|
"normal_session_recovery_supported": False,
|
|
"recovery_requirement": "explicit-read-only-reconciliation",
|
|
"runtime_bound": False,
|
|
"active_operation_id": None,
|
|
"record": {
|
|
"operation_id": operation_id,
|
|
"acquisition_id": "pending-reopened-acquisition",
|
|
"action": "stop",
|
|
"stage": "resolved",
|
|
"resolution": "not-dispatched",
|
|
"revision": 9,
|
|
"connection": {
|
|
"transport_ref": transport_ref,
|
|
"connection_mode": connection_mode,
|
|
},
|
|
"operator_retirements": [
|
|
{
|
|
"retirement_id": retirement_id,
|
|
"retired_transport_ref": transport_ref,
|
|
"original_attempt": {"operation_id": operation_id},
|
|
}
|
|
],
|
|
"operator_reconciliation_reopens": [
|
|
{
|
|
"retirement_id": retirement_id,
|
|
"retired_record_revision": 8,
|
|
}
|
|
],
|
|
"reconciliations": [],
|
|
},
|
|
}
|
|
|
|
|
|
def test_physical_recovery_policy_pins_original_ref_and_mode_but_keeps_scan_read_only(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_devices(
|
|
service,
|
|
[
|
|
{
|
|
"device_id": "k1-original",
|
|
"name": "Original K1",
|
|
"rssi": -40,
|
|
"address": None,
|
|
"connectable": True,
|
|
"likely_k1": True,
|
|
},
|
|
{
|
|
"device_id": "k1-nearby",
|
|
"name": "Nearby K1",
|
|
"rssi": -35,
|
|
"address": None,
|
|
"connectable": True,
|
|
"likely_k1": True,
|
|
},
|
|
],
|
|
)
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"snapshot",
|
|
lambda: _resolved_active_physical_snapshot(),
|
|
)
|
|
|
|
state = service.state()
|
|
policy = state["connection_policy"]["actions"]
|
|
|
|
assert policy["scan-ble"]["allowed"] is True
|
|
assert "physical-device-already-active" not in policy["scan-ble"]["reason_codes"]
|
|
assert policy["provision-fresh-device"]["allowed"] is False
|
|
assert "physical-device-already-active" in policy["provision-fresh-device"]["reason_codes"]
|
|
assert policy["observe-fresh-device-network"] == {
|
|
"allowed": True,
|
|
"reason_codes": [],
|
|
"target_source": "fresh-scan",
|
|
"required_transport_ref": "k1-original",
|
|
"required_connection_mode": "bridge",
|
|
"requires_live_gatt_validation": True,
|
|
"automatic_retry": False,
|
|
}
|
|
|
|
|
|
def test_physical_recovery_rejects_wrong_nearby_k1_before_topology_or_ble_io(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_devices(
|
|
service,
|
|
[
|
|
{
|
|
"device_id": "k1-original",
|
|
"name": "Original K1",
|
|
"rssi": -40,
|
|
"address": None,
|
|
"connectable": True,
|
|
"likely_k1": True,
|
|
},
|
|
{
|
|
"device_id": "k1-nearby",
|
|
"name": "Nearby K1",
|
|
"rssi": -35,
|
|
"address": None,
|
|
"connectable": True,
|
|
"likely_k1": True,
|
|
},
|
|
],
|
|
)
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"snapshot",
|
|
lambda: _resolved_active_physical_snapshot(),
|
|
)
|
|
io_calls: list[str] = []
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"read_wifi_status_once",
|
|
lambda *_args, **_kwargs: io_calls.append("gatt-read"),
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_apply_read_only_device_topology",
|
|
lambda *_args, **_kwargs: io_calls.append("topology-commit"),
|
|
)
|
|
semantic_before = service._semantic_topology_public_snapshot() # noqa: SLF001
|
|
supervisor_before = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
network_ledger_before = service._network_mutation_ledger.snapshot() # noqa: SLF001
|
|
|
|
with pytest.raises(facade_module.ConnectionVerificationError) as rejected:
|
|
asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="k1-nearby",
|
|
source="fresh-scan",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_discovery_generation=0,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert rejected.value.reason_code == "physical-command-recovery-target-mismatch"
|
|
assert io_calls == []
|
|
assert service._semantic_topology_public_snapshot() == semantic_before # noqa: SLF001
|
|
assert service._connection_supervisor.snapshot() == supervisor_before # noqa: SLF001
|
|
assert service._network_mutation_ledger.snapshot() == network_ledger_before # noqa: SLF001
|
|
|
|
|
|
def test_connect_is_fenced_before_gatt_or_wifi_when_physical_state_is_active(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-original")
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"snapshot",
|
|
lambda: _resolved_active_physical_snapshot(),
|
|
)
|
|
io_calls: list[str] = []
|
|
|
|
async def forbidden_network_io(*_args: object, **_kwargs: object) -> dict[str, Any]:
|
|
io_calls.append("network-io")
|
|
raise AssertionError("physical admission must fail before GATT/Wi-Fi")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_network_io)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_network_io)
|
|
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as rejected:
|
|
asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-original",
|
|
ssid="DCCONSTRUCTIONS",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
connection_mode="bridge",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert rejected.value.reason_code == "physical-device-already-active"
|
|
assert io_calls == []
|
|
|
|
|
|
def test_latest_scan_candidate_survives_wall_clock_age_but_requires_exact_capture(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
service._ble_device_last_seen_monotonic["k1-a"] = -1.0 # noqa: SLF001
|
|
service._ble_device_last_seen_suspend_aware["k1-a"] = -1.0 # noqa: SLF001
|
|
monkeypatch.setattr(facade_module, "_capture_network_intent_device", lambda _device_id: None)
|
|
|
|
async def forbidden_write(*_: object, **__: object) -> dict[str, Any]:
|
|
raise AssertionError("a missing exact scan capture must fail before GATT write")
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_write)
|
|
state = service.state()
|
|
|
|
assert [item["device_id"] for item in state["devices"]] == ["k1-a"]
|
|
assert state["selected_device_id"] is None
|
|
assert state["device_session"] is None
|
|
assert state["connection_lifecycle"]["active_binding"] is None
|
|
assert state["connection_lifecycle"]["active_binding_key"] is None
|
|
assert state["connection_policy"]["actions"]["provision-fresh-device"]["allowed"] is True
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as raised:
|
|
asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
connection_mode="bridge",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
assert raised.value.reason_code == "network-provision-candidate-not-fresh"
|
|
|
|
|
|
def test_candidate_wall_clock_age_does_not_close_an_admitted_connection_session(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_supervised_connection(service, transport_ref="k1-a")
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
service._ble_device_last_seen_monotonic["k1-a"] = -1.0 # noqa: SLF001
|
|
service._ble_device_last_seen_suspend_aware["k1-a"] = -1.0 # noqa: SLF001
|
|
|
|
state = service.state()
|
|
|
|
assert [item["device_id"] for item in state["devices"]] == ["k1-a"]
|
|
assert state["selected_device_id"] == "k1-a"
|
|
assert state["device_session"] is not None
|
|
assert state["device_session"]["connectivity"] in {"connected", "degraded"}
|
|
|
|
|
|
def test_ble_scan_lists_twenty_candidates_without_selecting_or_opening_gatt(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
low_level_calls: list[str] = []
|
|
|
|
async def twenty_candidate_scan(
|
|
_duration_seconds: float,
|
|
*,
|
|
on_admitted: Callable[[], None] | None = None,
|
|
) -> dict[str, Any]:
|
|
if on_admitted is not None:
|
|
on_admitted()
|
|
return {
|
|
"devices": [
|
|
{
|
|
"macos_uuid": f"K1-CANDIDATE-{index:02d}",
|
|
"name": f"XGR-K1-{index:02d}",
|
|
"local_name": f"XGR-K1-{index:02d}",
|
|
"rssi": -40 - index,
|
|
"k1_name_candidate": True,
|
|
}
|
|
for index in range(20)
|
|
]
|
|
}
|
|
|
|
async def forbidden_gatt(*_: object, **__: object) -> dict[str, Any]:
|
|
low_level_calls.append("gatt")
|
|
raise AssertionError("scan must not open GATT or write K1")
|
|
|
|
monkeypatch.setattr(facade_module, "scan", twenty_candidate_scan)
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_gatt)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_gatt)
|
|
|
|
state = asyncio.run(service.scan_ble(1.0))
|
|
|
|
assert len(state["devices"]) == 20
|
|
assert state["selected_device_id"] is None
|
|
assert state["device_session"] is None
|
|
assert state["connection_mode"] is None
|
|
assert low_level_calls == []
|
|
|
|
|
|
def _attempt_competing_scan_from_sync_boundary(
|
|
service: XgridsK1CompatibilityService,
|
|
*,
|
|
operation_id: str,
|
|
) -> object:
|
|
outcomes: list[object] = []
|
|
|
|
def worker() -> None:
|
|
try:
|
|
outcomes.append(
|
|
asyncio.run(
|
|
service.scan_ble(
|
|
BleScanRequest(
|
|
duration_seconds=1.0,
|
|
operation_id=operation_id,
|
|
)
|
|
)
|
|
)
|
|
)
|
|
except BaseException as exc:
|
|
outcomes.append(exc)
|
|
|
|
thread = threading.Thread(target=worker, daemon=True)
|
|
thread.start()
|
|
thread.join(timeout=1.0)
|
|
assert not thread.is_alive()
|
|
assert len(outcomes) == 1
|
|
return outcomes[0]
|
|
|
|
|
|
def test_explicit_new_scan_invalidates_previous_candidate_authority(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service)
|
|
|
|
async def empty_scan(
|
|
_duration_seconds: float,
|
|
*,
|
|
on_admitted: Callable[[], None] | None = None,
|
|
) -> dict[str, Any]:
|
|
assert on_admitted is not None
|
|
on_admitted()
|
|
return {"devices": []}
|
|
|
|
monkeypatch.setattr(facade_module, "scan", empty_scan)
|
|
|
|
state = asyncio.run(service.scan_ble(1.0))
|
|
|
|
assert state["devices"] == []
|
|
assert state["connection_policy"]["actions"]["provision-fresh-device"]["allowed"] is False
|
|
assert state["connection_policy"]["facts"]["fresh_transport_refs"] == []
|
|
|
|
|
|
def test_uuid_without_exact_latest_scan_capture_cannot_retire_existing_session(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
service._selected_device_id = "k1-a" # noqa: SLF001
|
|
service._connection_mode = "bridge" # noqa: SLF001
|
|
service._device_session_id = "existing-session" # noqa: SLF001
|
|
service._k1_ip = "192.168.68.50" # noqa: SLF001
|
|
monkeypatch.setattr(facade_module, "_capture_network_intent_device", lambda _device_id: None)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_retire_application_control_for_network_change",
|
|
lambda: (_ for _ in ()).throw(
|
|
AssertionError("missing exact capture must fail before ownership handoff")
|
|
),
|
|
)
|
|
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as raised:
|
|
asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
connection_mode="bridge",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert raised.value.reason_code == "network-provision-candidate-not-fresh"
|
|
assert service._selected_device_id == "k1-a" # noqa: SLF001
|
|
assert service._connection_mode == "bridge" # noqa: SLF001
|
|
assert service._device_session_id == "existing-session" # noqa: SLF001
|
|
assert service._k1_ip == "192.168.68.50" # noqa: SLF001
|
|
|
|
|
|
def test_stale_runtime_generation_cannot_revive_live_data_plane(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
runtime.source_mode = "live"
|
|
runtime.phase = "live"
|
|
runtime.producer_generation = 2
|
|
|
|
consumed = service._observe_runtime_message( # noqa: SLF001
|
|
StreamMessage(
|
|
sequence=7,
|
|
topic="RealtimePointcloud",
|
|
payload=b"stale",
|
|
received_at_epoch_ns=1,
|
|
received_monotonic_ns=1,
|
|
source="live_mqtt",
|
|
producer_generation=1,
|
|
),
|
|
BridgeMetrics(),
|
|
)
|
|
|
|
assert consumed is True
|
|
assert service._last_live_data_monotonic is None # noqa: SLF001
|
|
assert service._last_live_data_suspend_aware is None # noqa: SLF001
|
|
assert service._last_live_data_session_id is None # noqa: SLF001
|
|
|
|
|
|
@pytest.mark.parametrize("loss_boundary", ["control", "host"])
|
|
def test_bound_live_data_session_expires_after_control_or_host_lease_loss(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
loss_boundary: str,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(service)
|
|
with service._lock: # noqa: SLF001
|
|
service._compatibility_attestation = ATTESTATION.model_dump(mode="json") # noqa: SLF001
|
|
runtime.source_mode = "live"
|
|
runtime.phase = "live"
|
|
monotonic_now = [100.0]
|
|
monkeypatch.setattr(facade_module.time, "monotonic", lambda: monotonic_now[0])
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
data_session_id = "bound-live-data-session"
|
|
assert supervisor.observe_data_plane(
|
|
intent_id=binding.intent_id,
|
|
host_path_epoch=binding.host_path_epoch,
|
|
state="healthy",
|
|
session_id=data_session_id,
|
|
)
|
|
with service._lock: # noqa: SLF001
|
|
service._last_live_data_monotonic = monotonic_now[0] # noqa: SLF001
|
|
service._last_live_data_session_id = data_session_id # noqa: SLF001
|
|
|
|
if loss_boundary == "control":
|
|
assert supervisor.observe_control_loss(
|
|
intent_id=binding.intent_id,
|
|
host_path_epoch=binding.host_path_epoch,
|
|
control_session_id="test-control-test-ble-transport",
|
|
reason_code="test-control-loss",
|
|
)
|
|
else:
|
|
supervisor.observe_host_path(
|
|
HostPathProbeResult(
|
|
available=False,
|
|
fingerprint=None,
|
|
interface=None,
|
|
source_ipv4=None,
|
|
route_class="unavailable",
|
|
reason_code="test-host-loss",
|
|
)
|
|
)
|
|
|
|
revoked = supervisor.snapshot()
|
|
assert revoked.lease.state != "reachable"
|
|
assert revoked.data_plane.state == "healthy"
|
|
assert revoked.data_plane.host_path_epoch == binding.host_path_epoch
|
|
|
|
monotonic_now[0] += facade_module.LIVE_DATA_PLANE_STALL_SECONDS + 0.01
|
|
service._reconcile_connection_supervisor( # noqa: SLF001
|
|
{"state": "unknown"},
|
|
runtime.snapshot(),
|
|
)
|
|
stalled = supervisor.snapshot()
|
|
assert stalled.data_plane.state == "stalled"
|
|
assert stalled.data_plane.reason_code == "live-data-stalled"
|
|
assert stalled.data_plane.session_id == data_session_id
|
|
assert stalled.data_plane.host_path_epoch == binding.host_path_epoch
|
|
|
|
monotonic_now[0] = 100.0 + facade_module.LIVE_DATA_PLANE_LOST_SECONDS + 0.01
|
|
service._reconcile_connection_supervisor( # noqa: SLF001
|
|
{"state": "unknown"},
|
|
runtime.snapshot(),
|
|
)
|
|
lost = supervisor.snapshot()
|
|
assert lost.data_plane.state == "lost"
|
|
assert lost.data_plane.reason_code == "live-data-lost"
|
|
assert lost.data_plane.session_id == data_session_id
|
|
assert lost.data_plane.host_path_epoch == binding.host_path_epoch
|
|
|
|
assert (
|
|
supervisor.observe_data_plane(
|
|
intent_id=binding.intent_id,
|
|
host_path_epoch=binding.host_path_epoch,
|
|
state="healthy",
|
|
session_id="new-data-session-without-current-lease",
|
|
)
|
|
is False
|
|
)
|
|
|
|
|
|
def test_bound_live_data_session_expires_across_host_suspend(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(service)
|
|
runtime.source_mode = "live"
|
|
runtime.phase = "live"
|
|
monotonic_now = [300.0]
|
|
suspend_aware_now = [3_000.0]
|
|
monkeypatch.setattr(facade_module.time, "monotonic", lambda: monotonic_now[0])
|
|
monkeypatch.setattr(facade_module.time, "time", lambda: suspend_aware_now[0])
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
data_session_id = "pre-suspend-data-session"
|
|
assert supervisor.observe_data_plane(
|
|
intent_id=binding.intent_id,
|
|
host_path_epoch=binding.host_path_epoch,
|
|
state="healthy",
|
|
session_id=data_session_id,
|
|
)
|
|
with service._lock: # noqa: SLF001
|
|
service._last_live_data_monotonic = monotonic_now[0] # noqa: SLF001
|
|
service._last_live_data_suspend_aware = suspend_aware_now[0] # noqa: SLF001
|
|
service._last_live_data_session_id = data_session_id # noqa: SLF001
|
|
|
|
supervisor.observe_host_path(
|
|
HostPathProbeResult(
|
|
available=False,
|
|
fingerprint=None,
|
|
interface=None,
|
|
source_ipv4=None,
|
|
route_class="unavailable",
|
|
reason_code="host-suspended",
|
|
)
|
|
)
|
|
suspend_aware_now[0] += facade_module.LIVE_DATA_PLANE_STALL_SECONDS + 0.01
|
|
service._reconcile_connection_supervisor( # noqa: SLF001
|
|
{"state": "unknown"},
|
|
runtime.snapshot(),
|
|
)
|
|
assert supervisor.snapshot().data_plane.state == "stalled"
|
|
|
|
suspend_aware_now[0] = 3_000.0 + facade_module.LIVE_DATA_PLANE_LOST_SECONDS + 0.01
|
|
service._reconcile_connection_supervisor( # noqa: SLF001
|
|
{"state": "unknown"},
|
|
runtime.snapshot(),
|
|
)
|
|
lost = supervisor.snapshot()
|
|
assert lost.data_plane.state == "lost"
|
|
assert lost.data_plane.reason_code == "live-data-lost"
|
|
assert lost.data_plane.host_path_epoch == binding.host_path_epoch
|
|
|
|
|
|
def test_only_exact_bound_live_data_session_can_refresh_after_lease_loss(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(service)
|
|
runtime.source_mode = "live"
|
|
runtime.phase = "live"
|
|
runtime.source_ready = True
|
|
runtime.producer_generation = 4
|
|
monotonic_now = [200.0]
|
|
monkeypatch.setattr(facade_module.time, "monotonic", lambda: monotonic_now[0])
|
|
|
|
def published_point_cloud(sequence: int) -> DecodedPointCloudView:
|
|
return DecodedPointCloudView(
|
|
context=ConsumerFrameContext(
|
|
sequence=sequence,
|
|
captured_at_epoch_ns=sequence,
|
|
received_monotonic_ns=sequence,
|
|
processing_started_monotonic_ns=sequence,
|
|
encoded_size_bytes=16,
|
|
live=True,
|
|
),
|
|
frame_id="map",
|
|
positions_xyz=((0.0, 0.0, 0.0),),
|
|
)
|
|
|
|
runtime.pcl_frames = 1
|
|
service._observe_published_runtime_envelope( # noqa: SLF001
|
|
published_point_cloud(1),
|
|
runtime.producer_generation,
|
|
)
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
bound = supervisor.snapshot().data_plane
|
|
assert bound.state == "healthy"
|
|
assert bound.session_id == "test-session-test-ble-transport"
|
|
assert bound.host_path_epoch == binding.host_path_epoch
|
|
|
|
assert supervisor.observe_control_loss(
|
|
intent_id=binding.intent_id,
|
|
host_path_epoch=binding.host_path_epoch,
|
|
control_session_id="test-control-test-ble-transport",
|
|
reason_code="test-control-loss",
|
|
)
|
|
assert supervisor.observe_data_plane(
|
|
intent_id=binding.intent_id,
|
|
host_path_epoch=binding.host_path_epoch,
|
|
state="stalled",
|
|
session_id="test-session-test-ble-transport",
|
|
reason_code="test-data-stall",
|
|
)
|
|
|
|
monotonic_now[0] += 1.0
|
|
runtime.pcl_frames = 2
|
|
service._observe_published_runtime_envelope( # noqa: SLF001
|
|
published_point_cloud(2),
|
|
runtime.producer_generation,
|
|
)
|
|
refreshed = supervisor.snapshot()
|
|
assert refreshed.data_plane.state == "healthy"
|
|
assert refreshed.data_plane.session_id == "test-session-test-ble-transport"
|
|
assert refreshed.authority.control_allowed is False
|
|
assert refreshed.authority.data_ingest_authoritative is False
|
|
|
|
with service._lock: # noqa: SLF001
|
|
service._device_session_id = "new-session-without-current-lease" # noqa: SLF001
|
|
monotonic_now[0] += 1.0
|
|
runtime.pcl_frames = 3
|
|
service._observe_published_runtime_envelope( # noqa: SLF001
|
|
published_point_cloud(3),
|
|
runtime.producer_generation,
|
|
)
|
|
rejected_new_session = supervisor.snapshot()
|
|
assert rejected_new_session.data_plane.session_id == ("test-session-test-ble-transport")
|
|
assert service._last_live_data_session_id == ( # noqa: SLF001
|
|
"test-session-test-ble-transport"
|
|
)
|
|
|
|
|
|
def test_postpublished_pcl_rolls_same_session_to_fresh_control_epoch_after_wifi_rebind(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(service)
|
|
runtime.source_mode = "live"
|
|
runtime.phase = "live"
|
|
runtime.source_ready = True
|
|
runtime.producer_generation = 4
|
|
|
|
def published_point_cloud(sequence: int) -> DecodedPointCloudView:
|
|
return DecodedPointCloudView(
|
|
context=ConsumerFrameContext(
|
|
sequence=sequence,
|
|
captured_at_epoch_ns=sequence,
|
|
received_monotonic_ns=sequence,
|
|
processing_started_monotonic_ns=sequence,
|
|
encoded_size_bytes=16,
|
|
live=True,
|
|
),
|
|
frame_id="map",
|
|
positions_xyz=((0.0, 0.0, 0.0),),
|
|
)
|
|
|
|
runtime.pcl_frames = 1
|
|
service._observe_published_runtime_envelope( # noqa: SLF001
|
|
published_point_cloud(1),
|
|
runtime.producer_generation,
|
|
)
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
before_loss = supervisor.snapshot()
|
|
data_session_id = "test-session-test-ble-transport"
|
|
assert before_loss.host_path.epoch == binding.host_path_epoch
|
|
assert before_loss.data_plane.session_id == data_session_id
|
|
assert before_loss.data_plane.host_path_epoch == binding.host_path_epoch
|
|
|
|
supervisor.observe_host_path(
|
|
HostPathProbeResult(
|
|
available=False,
|
|
fingerprint=None,
|
|
interface=None,
|
|
source_ipv4=None,
|
|
route_class="unavailable",
|
|
reason_code="host-route-unavailable",
|
|
)
|
|
)
|
|
rebound_epoch = supervisor.observe_host_path(
|
|
HostPathProbeResult(
|
|
available=True,
|
|
fingerprint="route-after-wifi-return",
|
|
interface="test0",
|
|
source_ipv4="192.168.1.2",
|
|
route_class="direct",
|
|
kernel_route_fingerprint="route-after-wifi-return",
|
|
)
|
|
)
|
|
target = EndpointTarget(binding.target_ipv4, binding.target_port)
|
|
assert supervisor.observe_endpoint(
|
|
target=target,
|
|
intent_id=binding.intent_id,
|
|
host_path_epoch=rebound_epoch,
|
|
reachable=True,
|
|
)
|
|
assert supervisor.observe_control_evidence(
|
|
VerifiedControlEvidence(
|
|
intent_id=binding.intent_id,
|
|
transport_ref=binding.transport_ref,
|
|
host_path_epoch=rebound_epoch,
|
|
target=target,
|
|
connection_mode=binding.connection_mode,
|
|
logical_device_id="known-k1",
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
control_session_id="post-wifi-control",
|
|
control_proof_revision=2,
|
|
control_proof_source="correlated-application-response",
|
|
)
|
|
)
|
|
rebound = supervisor.snapshot()
|
|
assert rebound.host_path.epoch == rebound_epoch
|
|
assert rebound.authority.control_allowed is True
|
|
assert rebound.data_plane.session_id == data_session_id
|
|
assert rebound.data_plane.host_path_epoch == binding.host_path_epoch
|
|
assert rebound.authority.data_ingest_authoritative is False
|
|
|
|
runtime.pcl_frames = 2
|
|
service._observe_published_runtime_envelope( # noqa: SLF001
|
|
published_point_cloud(2),
|
|
runtime.producer_generation,
|
|
)
|
|
|
|
recovered = supervisor.snapshot()
|
|
assert recovered.data_plane.state == "healthy"
|
|
assert recovered.data_plane.session_id == data_session_id
|
|
assert recovered.data_plane.host_path_epoch == rebound_epoch
|
|
assert recovered.data_plane.host_path_epoch == recovered.host_path.epoch
|
|
assert recovered.authority.data_ingest_authoritative is True
|
|
|
|
|
|
def test_pose_only_traffic_cannot_refresh_point_cloud_health_during_recovery(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(service)
|
|
runtime.source_mode = "live"
|
|
runtime.phase = "live"
|
|
runtime.source_ready = True
|
|
runtime.producer_generation = 4
|
|
now = [100.0]
|
|
monkeypatch.setattr(facade_module.time, "monotonic", lambda: now[0])
|
|
monkeypatch.setattr(facade_module.time, "time", lambda: now[0])
|
|
|
|
def packet(sequence: int, topic: str) -> StreamMessage:
|
|
return StreamMessage(
|
|
sequence=sequence,
|
|
topic=topic,
|
|
payload=b"data-plane-evidence",
|
|
received_at_epoch_ns=sequence,
|
|
received_monotonic_ns=sequence,
|
|
source="live_mqtt",
|
|
producer_generation=4,
|
|
)
|
|
|
|
baseline = DecodedPointCloudView(
|
|
context=ConsumerFrameContext(
|
|
sequence=1,
|
|
captured_at_epoch_ns=1,
|
|
received_monotonic_ns=1,
|
|
processing_started_monotonic_ns=1,
|
|
encoded_size_bytes=16,
|
|
live=True,
|
|
),
|
|
frame_id="map",
|
|
positions_xyz=((0.0, 0.0, 0.0),),
|
|
)
|
|
runtime.pcl_frames = 1
|
|
service._observe_published_runtime_envelope( # noqa: SLF001
|
|
baseline,
|
|
runtime.producer_generation,
|
|
)
|
|
assert service._last_live_data_monotonic == 100.0 # noqa: SLF001
|
|
assert service._connection_supervisor.snapshot().data_plane.state == "healthy" # noqa: SLF001
|
|
|
|
runtime.phase = "reconnecting"
|
|
runtime.source_ready = False
|
|
runtime.recovery_state = "reconnecting"
|
|
|
|
now[0] += facade_module.LIVE_DATA_PLANE_STALL_SECONDS + 0.01
|
|
service._observe_runtime_message( # noqa: SLF001
|
|
packet(2, "lixel/application/report/lio_pose"),
|
|
BridgeMetrics(),
|
|
)
|
|
assert service._last_live_data_monotonic == 100.0 # noqa: SLF001
|
|
service._observe_runtime_message( # noqa: SLF001
|
|
packet(3, "RealtimePointcloud"),
|
|
BridgeMetrics(),
|
|
)
|
|
assert service._last_live_data_monotonic == 100.0 # noqa: SLF001
|
|
service._reconcile_connection_supervisor( # noqa: SLF001
|
|
{"state": "unknown"},
|
|
runtime.snapshot(),
|
|
)
|
|
assert service._connection_supervisor.snapshot().data_plane.state == "stalled" # noqa: SLF001
|
|
|
|
now[0] = 100.0 + facade_module.LIVE_DATA_PLANE_LOST_SECONDS + 0.01
|
|
service._observe_runtime_message( # noqa: SLF001
|
|
packet(4, "RealtimePath"),
|
|
BridgeMetrics(),
|
|
)
|
|
assert service._last_live_data_monotonic == 100.0 # noqa: SLF001
|
|
service._reconcile_connection_supervisor( # noqa: SLF001
|
|
{"state": "unknown"},
|
|
runtime.snapshot(),
|
|
)
|
|
lost = service._connection_supervisor.snapshot().data_plane # noqa: SLF001
|
|
assert lost.state == "lost"
|
|
assert lost.reason_code == "live-data-lost"
|
|
assert lost.host_path_epoch == binding.host_path_epoch
|
|
|
|
|
|
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,
|
|
*,
|
|
on_admitted: Callable[[], None] | None = None,
|
|
) -> dict[str, Any]:
|
|
if on_admitted is not None:
|
|
on_admitted()
|
|
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())
|
|
|
|
failed_state = service.state()
|
|
assert failed_state["devices"] == []
|
|
assert service._devices == [] # noqa: SLF001
|
|
assert service._ble_device_last_seen_monotonic == {} # noqa: SLF001
|
|
failed_operation = failed_state["last_operation"]
|
|
assert failed_operation["status"] == "failed"
|
|
assert failed_operation["error"] == {
|
|
"category": "transport",
|
|
"code": "RuntimeError",
|
|
"retryable": True,
|
|
"safe_to_retry": True,
|
|
"side_effect_status": "none",
|
|
}
|
|
|
|
|
|
def test_ble_scan_operation_id_is_exactly_once_and_request_bound(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
transport_calls = 0
|
|
|
|
async def successful_scan(
|
|
_duration_seconds: float,
|
|
*,
|
|
on_admitted: Callable[[], None] | None = None,
|
|
) -> dict[str, Any]:
|
|
nonlocal transport_calls
|
|
if on_admitted is not None:
|
|
on_admitted()
|
|
transport_calls += 1
|
|
return _ble_scan_result("exactly-once-device")
|
|
|
|
monkeypatch.setattr(facade_module, "scan", successful_scan)
|
|
operation_id = "op-00000000-0000-4000-8000-000000000001"
|
|
request = BleScanRequest(duration_seconds=6.0, operation_id=operation_id)
|
|
|
|
first = asyncio.run(service.scan_ble(request))
|
|
repeated = asyncio.run(service.scan_ble(request))
|
|
|
|
assert transport_calls == 1
|
|
assert first["devices"][0]["connectable"] is None
|
|
assert first["last_operation"]["operation_id"] == operation_id
|
|
assert repeated["last_operation"]["operation_id"] == operation_id
|
|
with pytest.raises(ValueError, match="different request"):
|
|
asyncio.run(
|
|
service.scan_ble(
|
|
BleScanRequest(
|
|
duration_seconds=7.0,
|
|
operation_id=operation_id,
|
|
)
|
|
)
|
|
)
|
|
|
|
|
|
def test_ble_scan_maps_process_runtime_busy_to_operation_journal(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="active-scan-result")
|
|
service._operation_phase = "scanning" # noqa: SLF001
|
|
|
|
async def scenario() -> tuple[Any, dict[str, Any]]:
|
|
async def busy_scan(
|
|
_duration_seconds: float,
|
|
*,
|
|
on_admitted: Callable[[], None] | None = None,
|
|
) -> dict[str, Any]:
|
|
del on_admitted
|
|
raise BleRuntimeBusy(
|
|
active_operation_kind="status-read",
|
|
cleanup_pending=False,
|
|
)
|
|
|
|
monkeypatch.setattr(facade_module, "scan", busy_scan)
|
|
with pytest.raises(BleRuntimeBusy, match="уже выполняется"):
|
|
await service.scan_ble(6.0)
|
|
assert service._operation_phase == "scanning" # noqa: SLF001
|
|
rejected_state = service.state()
|
|
|
|
async def recovered_scan(
|
|
_duration_seconds: float,
|
|
*,
|
|
on_admitted: Callable[[], None] | None = None,
|
|
) -> dict[str, Any]:
|
|
if on_admitted is not None:
|
|
on_admitted()
|
|
return _ble_scan_result("second-generation")
|
|
|
|
monkeypatch.setattr(facade_module, "scan", recovered_scan)
|
|
recovered_state = await service.scan_ble(6.0)
|
|
return rejected_state, recovered_state
|
|
|
|
rejected_state, recovered_state = asyncio.run(scenario())
|
|
|
|
assert [item["device_id"] for item in rejected_state["devices"]] == ["active-scan-result"]
|
|
rejected_operation = rejected_state["last_operation"]
|
|
assert rejected_operation["status"] == "failed"
|
|
assert rejected_operation["stage_code"] == "busy"
|
|
assert rejected_operation["message_code"] == "discovery.scan.already_running"
|
|
assert rejected_operation["error"] == {
|
|
"category": "conflict",
|
|
"code": "ble-runtime-busy",
|
|
"retryable": True,
|
|
"safe_to_retry": True,
|
|
"side_effect_status": "none",
|
|
}
|
|
assert [item["device_id"] for item in recovered_state["devices"]] == ["second-generation"]
|
|
|
|
|
|
def test_ble_scan_does_not_block_owner_loop_on_sync_lifecycle_gate(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
gate_held = threading.Event()
|
|
release_gate = threading.Event()
|
|
|
|
def hold_sync_lifecycle_gate() -> None:
|
|
with service._acquisition_lifecycle_gate: # noqa: SLF001
|
|
gate_held.set()
|
|
release_gate.wait(timeout=2.0)
|
|
|
|
holder = threading.Thread(target=hold_sync_lifecycle_gate)
|
|
holder.start()
|
|
assert gate_held.wait(timeout=1.0)
|
|
|
|
async def admitted_scan(
|
|
_duration_seconds: float,
|
|
*,
|
|
on_admitted: Callable[[], None] | None = None,
|
|
) -> dict[str, Any]:
|
|
if on_admitted is not None:
|
|
on_admitted()
|
|
release_gate.set()
|
|
return _ble_scan_result("nonblocking-scan")
|
|
|
|
monkeypatch.setattr(facade_module, "scan", admitted_scan)
|
|
started_at = time.monotonic()
|
|
started = asyncio.run(service.scan_ble(6.0))
|
|
elapsed = time.monotonic() - started_at
|
|
holder.join(timeout=1.0)
|
|
|
|
assert not holder.is_alive()
|
|
assert elapsed < 0.5
|
|
assert [item["device_id"] for item in started["devices"]] == ["nonblocking-scan"]
|
|
|
|
|
|
def test_ble_scan_maps_hard_timeout_to_operation_journal(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
|
|
async def scenario() -> tuple[dict[str, Any], dict[str, Any]]:
|
|
async def timed_out_scan(
|
|
_duration_seconds: float,
|
|
*,
|
|
on_admitted: Callable[[], None] | None = None,
|
|
) -> dict[str, Any]:
|
|
if on_admitted is not None:
|
|
on_admitted()
|
|
raise BleOperationHardTimeout(
|
|
"scan",
|
|
BleOperationProgress(operation_stage="discovery"),
|
|
)
|
|
|
|
monkeypatch.setattr(facade_module, "scan", timed_out_scan)
|
|
with pytest.raises(BleOperationHardTimeout, match="не завершилась"):
|
|
await service.scan_ble(6.0)
|
|
assert service._operation_phase is None # noqa: SLF001
|
|
failed_state = service.state()
|
|
|
|
async def recovered_scan(
|
|
_duration_seconds: float,
|
|
*,
|
|
on_admitted: Callable[[], None] | None = None,
|
|
) -> dict[str, Any]:
|
|
if on_admitted is not None:
|
|
on_admitted()
|
|
return _ble_scan_result("after-timeout")
|
|
|
|
monkeypatch.setattr(facade_module, "scan", recovered_scan)
|
|
recovered_state = await service.scan_ble(6.0)
|
|
return failed_state, recovered_state
|
|
|
|
failed_state, recovered_state = asyncio.run(scenario())
|
|
|
|
assert failed_state["devices"] == []
|
|
failed_operation = failed_state["last_operation"]
|
|
assert failed_operation["status"] == "failed"
|
|
assert failed_operation["error"] == {
|
|
"category": "transport",
|
|
"code": "ble-discovery-timeout",
|
|
"retryable": True,
|
|
"safe_to_retry": True,
|
|
"side_effect_status": "none",
|
|
"operation_stage": "discovery",
|
|
"device_write_attempted": False,
|
|
"device_write_confirmed": False,
|
|
"host_diagnostic": {
|
|
"schema_version": "missioncore.host-failure-diagnostic/v1",
|
|
"code": "host.bluetooth.operation-timeout",
|
|
"domain": "corebluetooth",
|
|
"impact": "discovery",
|
|
"operator_action": "explicit-retry",
|
|
"automatic_retry": False,
|
|
"redacted": True,
|
|
},
|
|
}
|
|
assert [item["device_id"] for item in recovered_state["devices"]] == ["after-timeout"]
|
|
|
|
|
|
def test_cancelled_ble_scan_is_journaled_and_releases_process_gate(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
|
|
async def scenario() -> tuple[dict[str, Any], dict[str, Any]]:
|
|
transport_entered = asyncio.Event()
|
|
transport_cancelled = asyncio.Event()
|
|
|
|
async def blocked_scan(
|
|
_duration_seconds: float,
|
|
*,
|
|
on_admitted: Callable[[], None] | None = None,
|
|
) -> dict[str, Any]:
|
|
if on_admitted is not None:
|
|
on_admitted()
|
|
transport_entered.set()
|
|
try:
|
|
await asyncio.Event().wait()
|
|
except asyncio.CancelledError:
|
|
transport_cancelled.set()
|
|
raise
|
|
|
|
monkeypatch.setattr(facade_module, "scan", blocked_scan)
|
|
scan_task = asyncio.create_task(service.scan_ble(6.0))
|
|
await asyncio.wait_for(transport_entered.wait(), timeout=1.0)
|
|
scan_task.cancel()
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await scan_task
|
|
assert transport_cancelled.is_set()
|
|
assert service._operation_phase is None # noqa: SLF001
|
|
cancelled_state = service.state()
|
|
|
|
async def recovered_scan(
|
|
_duration_seconds: float,
|
|
*,
|
|
on_admitted: Callable[[], None] | None = None,
|
|
) -> dict[str, Any]:
|
|
if on_admitted is not None:
|
|
on_admitted()
|
|
return _ble_scan_result("after-cancellation")
|
|
|
|
monkeypatch.setattr(facade_module, "scan", recovered_scan)
|
|
recovered_state = await service.scan_ble(6.0)
|
|
return cancelled_state, recovered_state
|
|
|
|
cancelled_state, recovered_state = asyncio.run(scenario())
|
|
|
|
assert cancelled_state["devices"] == []
|
|
cancelled_operation = cancelled_state["last_operation"]
|
|
assert cancelled_operation["status"] == "cancelled"
|
|
assert cancelled_operation["stage_code"] == "cancelled"
|
|
assert cancelled_operation["message_code"] == "discovery.scan.cancelled"
|
|
assert cancelled_operation["error"] == {
|
|
"category": "transport",
|
|
"code": "ble-discovery-cancelled",
|
|
"retryable": True,
|
|
"safe_to_retry": True,
|
|
"side_effect_status": "none",
|
|
}
|
|
assert [item["device_id"] for item in recovered_state["devices"]] == ["after-cancellation"]
|
|
|
|
|
|
def test_connect_owns_lifecycle_while_safe_preflight_rejects_a_competing_scan(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service)
|
|
mode_revision = _select_connection_mode(service, "quick-connect")
|
|
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": False,
|
|
"profile_enrolled": False,
|
|
"credential_source": "exact-firmware-profile",
|
|
}
|
|
|
|
async def replacement_scan(
|
|
_duration_seconds: float,
|
|
*,
|
|
on_admitted: Callable[[], None] | None = None,
|
|
) -> dict[str, Any]:
|
|
if on_admitted is not None:
|
|
on_admitted()
|
|
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(
|
|
_connect_request(
|
|
device_id="test-ble-transport",
|
|
connection_mode="quick-connect",
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
expected_mode_revision=mode_revision,
|
|
)
|
|
)
|
|
)
|
|
entered = await asyncio.wait_for(
|
|
asyncio.to_thread(preflight_entered.wait, 1.0),
|
|
timeout=1.5,
|
|
)
|
|
assert entered is True
|
|
with pytest.raises(
|
|
facade_module.BleDiscoveryUnavailable,
|
|
match="настройки Wi-Fi",
|
|
) as scan_failure:
|
|
await service.scan_ble(6.0)
|
|
assert scan_failure.value.reason_code == "ble-discovery-blocked-by-provisioning"
|
|
release_preflight.set()
|
|
with pytest.raises(facade_module.HostWifiProfileError):
|
|
await asyncio.wait_for(connect_task, timeout=1.0)
|
|
|
|
try:
|
|
asyncio.run(scenario())
|
|
finally:
|
|
release_preflight.set()
|
|
|
|
|
|
def test_provision_scan_is_rejected_after_atomic_fence_before_control_retirement(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
generation_before = service._ble_discovery_generation # noqa: SLF001
|
|
scan_outcomes: list[object] = []
|
|
provision_calls = 0
|
|
|
|
async def racing_scan(
|
|
_duration_seconds: float,
|
|
*,
|
|
on_admitted: Callable[[], None] | None = None,
|
|
) -> dict[str, Any]:
|
|
assert on_admitted is not None
|
|
on_admitted()
|
|
return _ble_scan_result("racing-scan")
|
|
|
|
original_retire = service._retire_application_control_for_network_change # noqa: SLF001
|
|
|
|
def retire_with_competing_scan() -> None:
|
|
assert service._provisioning_active is True # noqa: SLF001
|
|
scan_outcomes.append(
|
|
_attempt_competing_scan_from_sync_boundary(
|
|
service,
|
|
operation_id="op-00000000-0000-4000-8000-000000000101",
|
|
)
|
|
)
|
|
original_retire()
|
|
|
|
async def fake_provision(
|
|
*_: object,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**__: object,
|
|
) -> dict[str, Any]:
|
|
nonlocal provision_calls
|
|
provision_calls += 1
|
|
assert service._provisioning_active is True # noqa: SLF001
|
|
_dispatch_test_network_write(on_write_dispatch)
|
|
return {
|
|
"started_at_utc": "2026-08-06T12:00:00Z",
|
|
"completed_at_utc": "2026-08-06T12:00:01Z",
|
|
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
|
"outcome": "lan_address_observed",
|
|
"write_mode": "with_response",
|
|
"observations": [
|
|
{
|
|
"status": {
|
|
"mode": "WIFI_CLIENT",
|
|
"ipv4": "192.168.68.50",
|
|
"status_code": 1,
|
|
"reserved": 0,
|
|
}
|
|
}
|
|
],
|
|
}
|
|
|
|
monkeypatch.setattr(facade_module, "scan", racing_scan)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_retire_application_control_for_network_change",
|
|
retire_with_competing_scan,
|
|
)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", fake_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, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", lambda _target: True)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
_reachable_control_endpoint,
|
|
)
|
|
|
|
connected = asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
connection_mode="bridge",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert len(scan_outcomes) == 1
|
|
scan_error = scan_outcomes[0]
|
|
assert isinstance(scan_error, facade_module.BleDiscoveryUnavailable)
|
|
assert scan_error.reason_code == "ble-discovery-blocked-by-provisioning"
|
|
assert service._ble_discovery_generation == generation_before # noqa: SLF001
|
|
assert [item["device_id"] for item in connected["devices"]] == ["k1-a"]
|
|
assert provision_calls == 1
|
|
assert connected["connection_mode"] == "bridge"
|
|
assert service._provisioning_active is False # noqa: SLF001
|
|
scan_operation = next(
|
|
item
|
|
for item in connected["operations"]
|
|
if item["operation_id"] == "op-00000000-0000-4000-8000-000000000101"
|
|
)
|
|
assert scan_operation["status"] == "failed"
|
|
assert scan_operation["error"]["code"] == "ble-discovery-blocked-by-provisioning"
|
|
|
|
|
|
def test_corrupt_identity_pin_store_fails_after_new_intent_retires_old_control(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
retire_calls = 0
|
|
|
|
def corrupt_identity_lookup(_transport_ref: str) -> None:
|
|
raise DeviceIdentityPinStoreCorrupt("synthetic corrupt pin store")
|
|
|
|
def record_retirement() -> None:
|
|
nonlocal retire_calls
|
|
retire_calls += 1
|
|
|
|
async def forbidden_provision(*_: object, **__: object) -> dict[str, Any]:
|
|
raise AssertionError("identity preflight must fail before the BLE write boundary")
|
|
|
|
monkeypatch.setattr(service, "_expected_vendor_device_id", corrupt_identity_lookup)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_retire_application_control_for_network_change",
|
|
record_retirement,
|
|
)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_provision)
|
|
|
|
with pytest.raises(
|
|
DeviceIdentityPinStoreCorrupt,
|
|
match="synthetic corrupt pin store",
|
|
):
|
|
asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
connection_mode="bridge",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert retire_calls == 1
|
|
assert service._provisioning_active is False # noqa: SLF001
|
|
assert service._ble_discovery_generation == 0 # noqa: SLF001
|
|
assert [item["device_id"] for item in service.state()["devices"]] == ["k1-a"]
|
|
|
|
|
|
def test_unavailable_semantic_topology_store_fails_after_new_intent_retirement_without_write(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
service._semantic_topology_store = None # noqa: SLF001
|
|
retire_calls = 0
|
|
|
|
def record_retirement() -> None:
|
|
nonlocal retire_calls
|
|
retire_calls += 1
|
|
|
|
async def forbidden_provision(*_: object, **__: object) -> dict[str, Any]:
|
|
raise AssertionError("semantic store preflight must fail before the BLE write boundary")
|
|
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_retire_application_control_for_network_change",
|
|
record_retirement,
|
|
)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_provision)
|
|
|
|
with pytest.raises(
|
|
SemanticTopologyStoreCorrupt,
|
|
match="semantic topology store is unavailable",
|
|
):
|
|
asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
connection_mode="bridge",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert retire_calls == 1
|
|
assert service._provisioning_active is False # noqa: SLF001
|
|
assert service._provisioning_gate.locked() is False # noqa: SLF001
|
|
assert service._ble_discovery_generation == 0 # noqa: SLF001
|
|
assert [item["device_id"] for item in service.state()["devices"]] == ["k1-a"]
|
|
|
|
|
|
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)
|
|
# Ordinary topology adoption must still retire unrelated terminal local
|
|
# history. Only a later exact physical SCANNING reconciliation may
|
|
# materialize the bounded recovery-only STOP identity.
|
|
service._acquisition = facade_module.AcquisitionRecord( # noqa: SLF001
|
|
acquisition_id="stale-terminal-acquisition",
|
|
device_id="stale-device",
|
|
device_session_id="stale-device-session",
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
control_mode="plugin-commanded",
|
|
requested_streams=(),
|
|
target_host="192.168.68.99",
|
|
duration_seconds=None,
|
|
evidence_policy="disabled",
|
|
state="failed",
|
|
)
|
|
service._acquisition_project_name = "STALE" # noqa: SLF001
|
|
_set_scanned_k1(service)
|
|
status_reads: list[tuple[str, float, bool]] = []
|
|
|
|
async def fake_status_read(
|
|
device_id: str,
|
|
*,
|
|
timeout_seconds: float,
|
|
rediscover: bool,
|
|
**_: object,
|
|
) -> 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)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
|
|
state = asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="test-ble-transport",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_discovery_generation=0,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert status_reads == [("test-ble-transport", 20.0, False)]
|
|
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": "reachable",
|
|
"lease_state": "reachable",
|
|
"lease_generation": state["connection_supervisor"]["lease"]["generation"],
|
|
"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",
|
|
"reason_code": None,
|
|
"supervisor_revision": state["connection_verification"]["supervisor_revision"],
|
|
}
|
|
assert state["last_operation"]["action"] == "connection.verify"
|
|
assert state["last_operation"]["status"] == "succeeded"
|
|
assert state["last_operation"]["result"]["write_performed"] is False
|
|
assert state["last_operation"]["result"]["control_verified"] is True
|
|
assert state["connection_lifecycle"]["connection_ready"] is True
|
|
assert state["connection_lifecycle"]["active_mode"] == "bridge"
|
|
assert state["acquisition"] is None
|
|
assert service._acquisition_project_name is None # noqa: SLF001
|
|
|
|
|
|
def test_read_only_verify_scan_is_rejected_after_atomic_fence_before_admission(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service)
|
|
generation_before = service._ble_discovery_generation # noqa: SLF001
|
|
scan_outcomes: list[object] = []
|
|
admission_calls = 0
|
|
|
|
async def racing_scan(
|
|
_duration_seconds: float,
|
|
*,
|
|
on_admitted: Callable[[], None] | None = None,
|
|
) -> dict[str, Any]:
|
|
assert on_admitted is not None
|
|
on_admitted()
|
|
return _ble_scan_result("racing-scan")
|
|
|
|
async def fake_status_read(*_: object, **__: object) -> dict[str, Any]:
|
|
return _wifi_status_read("10.255.254.77")
|
|
|
|
original_retire = service._retire_application_control_for_network_change # noqa: SLF001
|
|
original_apply = service._apply_read_only_device_topology # noqa: SLF001
|
|
|
|
def retire_with_competing_scan(*, allow_terminal_failure: bool = False) -> None:
|
|
assert service._provisioning_active is True # noqa: SLF001
|
|
scan_outcomes.append(
|
|
_attempt_competing_scan_from_sync_boundary(
|
|
service,
|
|
operation_id="op-00000000-0000-4000-8000-000000000102",
|
|
)
|
|
)
|
|
original_retire(allow_terminal_failure=allow_terminal_failure)
|
|
|
|
def apply_with_fence_assertion(**kwargs: Any) -> str:
|
|
nonlocal admission_calls
|
|
admission_calls += 1
|
|
assert service._provisioning_active is True # noqa: SLF001
|
|
return original_apply(**kwargs)
|
|
|
|
monkeypatch.setattr(facade_module, "scan", racing_scan)
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", fake_status_read)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_retire_application_control_for_network_change",
|
|
retire_with_competing_scan,
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_apply_read_only_device_topology",
|
|
apply_with_fence_assertion,
|
|
)
|
|
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)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
|
|
verified = asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="test-ble-transport",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_discovery_generation=0,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert len(scan_outcomes) == 1
|
|
scan_error = scan_outcomes[0]
|
|
assert isinstance(scan_error, facade_module.BleDiscoveryUnavailable)
|
|
assert scan_error.reason_code == "ble-discovery-blocked-by-provisioning"
|
|
assert service._ble_discovery_generation == generation_before # noqa: SLF001
|
|
assert [item["device_id"] for item in verified["devices"]] == ["test-ble-transport"]
|
|
assert admission_calls == 1
|
|
assert verified["connection_mode"] == "bridge"
|
|
assert verified["k1_ip"] == "10.255.254.77"
|
|
assert service._provisioning_active is False # noqa: SLF001
|
|
scan_operation = next(
|
|
item
|
|
for item in verified["operations"]
|
|
if item["operation_id"] == "op-00000000-0000-4000-8000-000000000102"
|
|
)
|
|
assert scan_operation["status"] == "failed"
|
|
assert scan_operation["error"]["code"] == "ble-discovery-blocked-by-provisioning"
|
|
|
|
|
|
def test_connection_verify_operation_id_is_exactly_once_and_request_bound(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service)
|
|
status_reads = 0
|
|
|
|
async def fake_status_read(*_: object, **__: object) -> dict[str, Any]:
|
|
nonlocal status_reads
|
|
status_reads += 1
|
|
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)
|
|
monkeypatch.setattr(facade_module, "_host_route_class", lambda _target: "direct-or-routed")
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", lambda _target: True)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
operation_id = "op-00000000-0000-4000-8000-000000000002"
|
|
request = ConnectionVerifyRequest(
|
|
device_id="test-ble-transport",
|
|
compatibility_attestation=ATTESTATION,
|
|
operation_id=operation_id,
|
|
expected_discovery_generation=0,
|
|
)
|
|
|
|
first = asyncio.run(service.verify_connection(request))
|
|
repeated = asyncio.run(service.verify_connection(request))
|
|
|
|
assert status_reads == 1
|
|
assert first["last_operation"]["operation_id"] == operation_id
|
|
assert repeated["last_operation"]["operation_id"] == operation_id
|
|
assert first["last_operation"]["result"]["lease_state"] == "reachable"
|
|
assert first["last_operation"]["result"]["lease_generation"] == (
|
|
first["connection_supervisor"]["lease"]["generation"]
|
|
)
|
|
with pytest.raises(ValueError, match="different request"):
|
|
asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="different-k1",
|
|
compatibility_attestation=ATTESTATION,
|
|
operation_id=operation_id,
|
|
expected_discovery_generation=0,
|
|
)
|
|
)
|
|
)
|
|
|
|
|
|
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(
|
|
facade_module.ConnectionVerificationError,
|
|
match="свежего кандидата",
|
|
) as raised:
|
|
asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="not-in-current-scan",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_discovery_generation=0,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert raised.value.reason_code == "connection-verify-candidate-not-fresh"
|
|
operation = service.state()["last_operation"]
|
|
assert operation["action"] == "connection.verify"
|
|
assert operation["status"] == "failed"
|
|
assert operation["error"] == {
|
|
"category": "connection",
|
|
"code": "connection-verify-candidate-not-fresh",
|
|
"retryable": True,
|
|
"safe_to_retry": True,
|
|
"side_effect_status": "none",
|
|
}
|
|
|
|
|
|
def test_verify_connection_timeout_is_journaled_without_device_side_effect(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service)
|
|
|
|
async def timed_out_status_read(*_: object, **__: object) -> dict[str, Any]:
|
|
failure = TimeoutError("synthetic private transport detail")
|
|
failure.operation_stage = "exact-uuid-scan" # type: ignore[attr-defined]
|
|
raise failure
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", timed_out_status_read)
|
|
|
|
with (
|
|
caplog.at_level(logging.INFO, logger=facade_module.__name__),
|
|
pytest.raises(
|
|
facade_module.ConnectionVerificationError,
|
|
match="не завершил ожидание точного",
|
|
) as raised,
|
|
):
|
|
asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="test-ble-transport",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_discovery_generation=0,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert raised.value.reason_code == "connection-verify-exact-uuid-scan-timeout"
|
|
operation = service.state()["last_operation"]
|
|
assert operation["error"] == {
|
|
"category": "connection",
|
|
"code": "connection-verify-exact-uuid-scan-timeout",
|
|
"retryable": True,
|
|
"safe_to_retry": True,
|
|
"side_effect_status": "none",
|
|
"operation_stage": "exact-uuid-scan",
|
|
}
|
|
accepted_at = datetime.fromisoformat(operation["accepted_at"])
|
|
deadline_at = datetime.fromisoformat(operation["deadline_at"])
|
|
assert (deadline_at - accepted_at).total_seconds() == (
|
|
facade_module.CONNECTION_VERIFY_HARD_TIMEOUT_SECONDS
|
|
)
|
|
failure_log = next(
|
|
record
|
|
for record in caplog.records
|
|
if getattr(record, "event_code", None) == "k1_connection_verify_failed"
|
|
)
|
|
assert failure_log.operation_stage == "exact-uuid-scan"
|
|
assert failure_log.reason_code == "connection-verify-exact-uuid-scan-timeout"
|
|
assert failure_log.failure_category == "timeout"
|
|
assert failure_log.device_write_performed is False
|
|
assert failure_log.automatic_retry is False
|
|
assert "private" not in str(operation)
|
|
assert "private" not in caplog.text
|
|
|
|
|
|
def test_connection_lease_probe_race_is_an_expected_state_conflict() -> None:
|
|
failure = facade_module._connection_verification_error( # noqa: SLF001
|
|
facade_module.ConnectionLeaseUnavailable(
|
|
"private concurrent lease mutation detail",
|
|
reason_code="connection_lease_changed_during_probe",
|
|
)
|
|
)
|
|
|
|
assert failure.reason_code == "connection-verify-lease-changed"
|
|
assert str(failure) == "Подключение K1 изменилось во время read-only проверки"
|
|
assert "private" not in str(failure)
|
|
|
|
|
|
def test_read_only_gatt_contract_is_logged_before_missing_dhcp_rejection(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service)
|
|
|
|
async def status_without_lan_address(*_: object, **__: object) -> dict[str, Any]:
|
|
return _wifi_status_read(None)
|
|
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"read_wifi_status_once",
|
|
status_without_lan_address,
|
|
)
|
|
|
|
with (
|
|
caplog.at_level(logging.INFO, logger=facade_module.__name__),
|
|
pytest.raises(facade_module.ConnectionVerificationError) as raised,
|
|
):
|
|
asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="test-ble-transport",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_discovery_generation=0,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert raised.value.reason_code == "connection-verify-address-unavailable"
|
|
contract_log = next(
|
|
record
|
|
for record in caplog.records
|
|
if getattr(record, "event_code", None) == "k1_ble_gatt_contract_observed"
|
|
)
|
|
assert contract_log.write_characteristic_properties == ["read", "write"]
|
|
assert contract_log.max_write_without_response_size == 253
|
|
assert contract_log.mtu_size == 256
|
|
assert contract_log.bridge_frame_length == 99
|
|
assert contract_log.quick_connect_frame_length == 100
|
|
assert contract_log.device_write_performed is False
|
|
assert contract_log.automatic_retry is False
|
|
assert "device_macos_uuid" not in contract_log.__dict__
|
|
assert "write_characteristic_uuid" not in contract_log.__dict__
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("cleanup_pending", "reason_code"),
|
|
[
|
|
(False, "connection-verify-busy"),
|
|
(True, "connection-verify-cleanup-pending"),
|
|
],
|
|
)
|
|
def test_verify_connection_preserves_process_ble_busy_state(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
cleanup_pending: bool,
|
|
reason_code: str,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service)
|
|
|
|
async def busy_status_read(*_: object, **__: object) -> dict[str, Any]:
|
|
raise BleRuntimeBusy(
|
|
active_operation_kind="scan",
|
|
cleanup_pending=cleanup_pending,
|
|
)
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", busy_status_read)
|
|
|
|
with pytest.raises(facade_module.ConnectionVerificationError) as raised:
|
|
asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="test-ble-transport",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_discovery_generation=0,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert raised.value.reason_code == reason_code
|
|
operation = service.state()["last_operation"]
|
|
assert operation["error"] == {
|
|
"category": "connection",
|
|
"code": reason_code,
|
|
"retryable": True,
|
|
"safe_to_retry": True,
|
|
"side_effect_status": "none",
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("ipv4", "is_local", "route_class", "endpoint_reachable", "reason_code"),
|
|
[
|
|
(None, False, "direct-or-routed", True, "connection-verify-address-unavailable"),
|
|
(
|
|
"192.168.56.1",
|
|
False,
|
|
"device-ap",
|
|
True,
|
|
"connection-verify-address-unavailable",
|
|
),
|
|
(
|
|
"10.255.254.77",
|
|
True,
|
|
"direct-or-routed",
|
|
True,
|
|
"connection-verify-local-address-conflict",
|
|
),
|
|
("10.255.254.77", False, "tunnel", True, "connection-verify-route-mismatch"),
|
|
(
|
|
"10.255.254.77",
|
|
False,
|
|
"default-route",
|
|
True,
|
|
"connection-verify-route-mismatch",
|
|
),
|
|
(
|
|
"10.255.254.77",
|
|
False,
|
|
"direct-or-routed",
|
|
False,
|
|
"connection-verify-mqtt-unreachable",
|
|
),
|
|
],
|
|
)
|
|
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,
|
|
reason_code: 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,
|
|
)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
|
|
with pytest.raises(facade_module.ConnectionVerificationError) as raised:
|
|
asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="test-ble-transport",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_discovery_generation=0,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert raised.value.reason_code == reason_code
|
|
|
|
state = service.state()
|
|
assert [item["device_id"] for item in state["devices"]] == ["test-ble-transport"]
|
|
# Fresh Bridge 7f02 evidence is provisional until route/TCP and exact
|
|
# DeviceInfo identity all succeed. Any failure before that point retires
|
|
# only the ephemeral projection, retains the fresh candidate for a later
|
|
# explicit choice, and never publishes untrusted durable topology.
|
|
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["semantic_topology_store"]["status"] == "empty"
|
|
assert state["connection_supervisor"]["authority"]["control_allowed"] is False
|
|
assert state["connection_verification"].get("write_performed") is not True
|
|
|
|
|
|
def test_apply_owned_rescan_after_unreachable_verify_dispatches_exactly_one_write(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
device_id = "k1-verify-then-apply"
|
|
_set_scanned_k1(service, device_id=device_id)
|
|
old_capture = _SYNTHETIC_SCAN_CAPTURES[device_id]
|
|
replacement_capture = facade_module.CapturedDiscoveredDevice(
|
|
device=SimpleNamespace(address=device_id), # type: ignore[arg-type]
|
|
macos_uuid=device_id,
|
|
owner_epoch=old_capture.owner_epoch,
|
|
scan_generation=old_capture.scan_generation + 1,
|
|
)
|
|
active_capture = old_capture
|
|
invalidated_capture_ids: set[int] = set()
|
|
pinned_by_session: dict[str, facade_module.CapturedDiscoveredDevice] = {}
|
|
|
|
def exact_capture(requested_device_id: str) -> object | None:
|
|
assert requested_device_id == device_id
|
|
if id(active_capture) in invalidated_capture_ids:
|
|
return None
|
|
return active_capture
|
|
|
|
def pin_exact_capture(
|
|
capture: facade_module.CapturedDiscoveredDevice,
|
|
*,
|
|
device_session_id: str,
|
|
) -> None:
|
|
assert capture is active_capture
|
|
pinned_by_session[device_session_id] = capture
|
|
|
|
def invalidate_exact_session(
|
|
requested_device_id: str,
|
|
*,
|
|
device_session_id: str,
|
|
) -> bool:
|
|
assert requested_device_id == device_id
|
|
retired = pinned_by_session.pop(device_session_id, None)
|
|
if retired is None:
|
|
return False
|
|
invalidated_capture_ids.add(id(retired))
|
|
return True
|
|
|
|
async def status_on_unreachable_existing_network(
|
|
*_: object,
|
|
captured_device: facade_module.CapturedDiscoveredDevice | None = None,
|
|
on_gatt_validated: Callable[[facade_module.CapturedDiscoveredDevice], None] | None = None,
|
|
**__: object,
|
|
) -> dict[str, Any]:
|
|
assert captured_device is old_capture
|
|
assert on_gatt_validated is not None
|
|
on_gatt_validated(old_capture)
|
|
return _wifi_status_read("192.168.1.20", device_id=device_id)
|
|
|
|
monkeypatch.setattr(facade_module, "_capture_network_intent_device", exact_capture)
|
|
monkeypatch.setattr(facade_module, "capture_discovered_device", exact_capture)
|
|
monkeypatch.setattr(facade_module, "pin_connected_device_handle", pin_exact_capture)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"invalidate_connected_device_session",
|
|
invalidate_exact_session,
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"read_wifi_status_once",
|
|
status_on_unreachable_existing_network,
|
|
)
|
|
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, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_control_endpoint_reachable",
|
|
lambda _target: False,
|
|
)
|
|
|
|
with pytest.raises(facade_module.ConnectionVerificationError) as unavailable:
|
|
asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id=device_id,
|
|
source="fresh-scan",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_discovery_generation=0,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert unavailable.value.reason_code == "connection-verify-mqtt-unreachable"
|
|
after_verify = service.state()
|
|
assert [item["device_id"] for item in after_verify["devices"]] == [device_id]
|
|
assert after_verify["selected_device_id"] is None
|
|
assert invalidated_capture_ids == {id(old_capture)}
|
|
assert exact_capture(device_id) is None
|
|
|
|
async def apply_owned_scan(
|
|
_duration_seconds: float,
|
|
*,
|
|
on_admitted: Callable[[], None] | None = None,
|
|
) -> dict[str, Any]:
|
|
nonlocal active_capture
|
|
assert on_admitted is not None
|
|
on_admitted()
|
|
active_capture = replacement_capture
|
|
return _ble_scan_result(device_id)
|
|
|
|
monkeypatch.setattr(facade_module, "scan", apply_owned_scan)
|
|
rescanned = asyncio.run(service.scan_ble(BleScanRequest(duration_seconds=1.0)))
|
|
assert rescanned["ble_discovery_generation"] == 1
|
|
assert exact_capture(device_id) is replacement_capture
|
|
|
|
dispatched_writes: list[facade_module.CapturedDiscoveredDevice] = []
|
|
|
|
async def provision_once(
|
|
*_: object,
|
|
captured_device: facade_module.CapturedDiscoveredDevice | None = None,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**__: object,
|
|
) -> dict[str, Any]:
|
|
assert captured_device is replacement_capture
|
|
_dispatch_test_network_write(on_write_dispatch)
|
|
dispatched_writes.append(captured_device)
|
|
return {
|
|
"started_at_utc": "2026-08-11T07:00:00Z",
|
|
"completed_at_utc": "2026-08-11T07:00:01Z",
|
|
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
|
"outcome": "lan_address_observed",
|
|
"write_mode": "with_response",
|
|
"baseline_status": _wifi_status_read(None)["status"],
|
|
"observations": [{"status": _wifi_status_read("192.168.1.20")["status"]}],
|
|
}
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", provision_once)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_control_endpoint_reachable",
|
|
lambda _target: True,
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
_reachable_control_endpoint,
|
|
)
|
|
connected = asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id=device_id,
|
|
ssid="lab-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_discovery_generation=rescanned["ble_discovery_generation"],
|
|
)
|
|
)
|
|
)
|
|
|
|
assert dispatched_writes == [replacement_capture]
|
|
assert connected["connection_lifecycle"]["connection_ready"] is True
|
|
network_operations = [
|
|
item for item in connected["operations"] if item["action"] == "network.provision"
|
|
]
|
|
assert len(network_operations) == 1
|
|
assert network_operations[0]["status"] == "succeeded"
|
|
verify_operations = [
|
|
item for item in connected["operations"] if item["action"] == "connection.verify"
|
|
]
|
|
assert len(verify_operations) == 1
|
|
assert verify_operations[0]["status"] == "failed"
|
|
assert verify_operations[0]["error"]["side_effect_status"] == "none"
|
|
|
|
|
|
def test_connection_verify_request_requires_paired_exact_firmware_attestation() -> None:
|
|
with pytest.raises(ValidationError, match="compatibility_attestation"):
|
|
ConnectionVerifyRequest(device_id="test-ble-transport")
|
|
|
|
with pytest.raises(ValidationError, match="device_id"):
|
|
ConnectionVerifyRequest(compatibility_attestation=ATTESTATION)
|
|
|
|
with pytest.raises(ValidationError, match="3.0.2"):
|
|
ConnectionVerifyRequest(
|
|
device_id="test-ble-transport",
|
|
compatibility_attestation={
|
|
"firmware_version": "3.0.3",
|
|
"topology": "direct-lan",
|
|
"verification": "live-device-info",
|
|
},
|
|
)
|
|
|
|
with pytest.raises(ValidationError, match="expected_discovery_generation"):
|
|
ConnectionVerifyRequest(
|
|
device_id="test-ble-transport",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
|
|
quick_connect = ConnectionVerifyRequest(
|
|
device_id="test-ble-transport",
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
expected_discovery_generation=0,
|
|
)
|
|
assert quick_connect.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
|
|
service._compatibility_attestation = ATTESTATION.model_dump(mode="json") # noqa: SLF001
|
|
_set_scanned_k1(service)
|
|
|
|
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)
|
|
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",
|
|
)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
|
|
state = asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="test-ble-transport",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_discovery_generation=0,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert state["k1_ip"] == "10.255.254.77"
|
|
assert state["device_session"]["device_session_id"] != "old-device-session"
|
|
assert state["connection_verification"] == {
|
|
"status": "reachable",
|
|
"lease_state": "reachable",
|
|
"lease_generation": state["connection_supervisor"]["lease"]["generation"],
|
|
"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": True,
|
|
"write_performed": False,
|
|
"observed_at": "2026-07-20T12:00:00Z",
|
|
"reason_code": None,
|
|
"supervisor_revision": state["connection_verification"]["supervisor_revision"],
|
|
}
|
|
assert state["device_calibration"]["status"] == "unavailable"
|
|
|
|
|
|
def test_bridge_read_only_verify_never_shortcuts_fresh_ble_status(
|
|
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._compatibility_attestation = ATTESTATION.model_dump(mode="json") # noqa: SLF001
|
|
service._device_id = "known-k1" # noqa: SLF001
|
|
service._device_session_id = "known-device-session" # noqa: SLF001
|
|
_set_scanned_k1(service)
|
|
_seed_supervised_connection(
|
|
service,
|
|
target_ipv4="10.255.254.54",
|
|
with_control=False,
|
|
)
|
|
service._device_session_id = "known-device-session" # noqa: SLF001
|
|
|
|
monkeypatch.setattr(facade_module, "_host_route_class", lambda _target: "direct-or-routed")
|
|
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", lambda _target: True)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
|
|
status_reads: list[tuple[str, float, bool]] = []
|
|
|
|
async def exact_status_read(
|
|
device_id: str,
|
|
*,
|
|
timeout_seconds: float,
|
|
rediscover: bool,
|
|
**_: object,
|
|
) -> dict[str, Any]:
|
|
status_reads.append((device_id, timeout_seconds, rediscover))
|
|
return _wifi_status_read("10.255.254.54")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", exact_status_read)
|
|
|
|
state = asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="test-ble-transport",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_discovery_generation=0,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert status_reads == [("test-ble-transport", 20.0, False)]
|
|
assert state["k1_ip"] == "10.255.254.54"
|
|
# A fresh exact BLE observation creates a new intent/session fence even
|
|
# when DHCP returned the same address. Similar endpoint text is not
|
|
# continuity authority.
|
|
assert state["device_session"]["device_session_id"] != "known-device-session"
|
|
assert state["connection_verification"] == {
|
|
"status": "reachable",
|
|
"lease_state": "reachable",
|
|
"lease_generation": state["connection_supervisor"]["lease"]["generation"],
|
|
"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": False,
|
|
"previous_address_present": True,
|
|
"write_performed": False,
|
|
"observed_at": "2026-07-20T12:00:00Z",
|
|
"reason_code": None,
|
|
"supervisor_revision": state["connection_verification"]["supervisor_revision"],
|
|
}
|
|
assert state["last_operation"]["status"] == "succeeded"
|
|
|
|
|
|
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
|
|
service._compatibility_attestation = ATTESTATION.model_dump(mode="json") # noqa: SLF001
|
|
status_loop: asyncio.AbstractEventLoop | None = None
|
|
runtime_loop: asyncio.AbstractEventLoop | None = None
|
|
|
|
async def fake_status_read(*_: object, **__: object) -> dict[str, Any]:
|
|
nonlocal status_loop
|
|
status_loop = asyncio.get_running_loop()
|
|
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)
|
|
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",
|
|
)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
|
|
async def scenario() -> dict[str, Any]:
|
|
nonlocal runtime_loop
|
|
runtime_loop = asyncio.get_running_loop()
|
|
service.bind_runtime_event_loop()
|
|
return await asyncio.to_thread(
|
|
service.prepare_acquisition,
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
compatibility_attestation=ATTESTATION,
|
|
),
|
|
)
|
|
|
|
state = asyncio.run(scenario())
|
|
|
|
assert state["k1_ip"] == "10.255.254.77"
|
|
assert state["acquisition"]["target_host"] == "10.255.254.77"
|
|
assert status_loop is runtime_loop
|
|
|
|
|
|
def test_control_session_reuses_reachable_process_owned_connection_lease(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_supervised_connection(
|
|
service,
|
|
target_ipv4="10.255.254.54",
|
|
with_control=False,
|
|
)
|
|
service._device_session_id = "known-session" # noqa: SLF001
|
|
service._compatibility_attestation = ATTESTATION.model_dump(mode="json") # noqa: SLF001
|
|
opened_hosts: list[str] = []
|
|
|
|
class FakeOpenControlSession:
|
|
def __init__(self) -> None:
|
|
self.state = "idle"
|
|
self.verified_control: dict[str, object] | None = None
|
|
|
|
def snapshot(self) -> dict[str, object]:
|
|
return {
|
|
"state": self.state,
|
|
"can_confirm_standby": False,
|
|
"verified_control": self.verified_control,
|
|
}
|
|
|
|
def open(
|
|
self,
|
|
*,
|
|
host: str,
|
|
connection_binding: ApplicationConnectionBinding,
|
|
**_: object,
|
|
) -> dict[str, object]:
|
|
opened_hosts.append(host)
|
|
self.state = "connection-ready"
|
|
self.verified_control = _verified_control_for_binding(connection_binding)
|
|
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)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
admission_generation = service._connection_supervisor.snapshot().lease.generation # noqa: SLF001
|
|
|
|
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": admission_generation,
|
|
"connection_lease_reused": True,
|
|
"recovery_performed": False,
|
|
"address_changed": False,
|
|
"device_write_performed": False,
|
|
}
|
|
service._release_application_control_process_lease() # noqa: SLF001
|
|
|
|
|
|
def test_operator_control_open_replaces_verify_inspection_with_fresh_dialogue(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
transitions: list[str] = []
|
|
|
|
class RecordingControlSession(FakeInteractiveControlSession):
|
|
def close_prestart(self, **kwargs: object) -> dict[str, object]:
|
|
transitions.append("inspection-closed")
|
|
return super().close_prestart(**kwargs)
|
|
|
|
def retire_for_network_change(self, **kwargs: object) -> dict[str, object]:
|
|
transitions.append("inspection-retired")
|
|
return super().retire_for_network_change(**kwargs)
|
|
|
|
def open(self, *, inspection_only: bool = False, **kwargs: object) -> dict[str, object]:
|
|
transitions.append(
|
|
"inspection-opened" if inspection_only else "canonical-opened"
|
|
)
|
|
self.session_generation += 1
|
|
return super().open(inspection_only=inspection_only, **kwargs)
|
|
|
|
control = RecordingControlSession(initial_state="connection-ready")
|
|
control.session_generation = 7
|
|
control.state_revision = 11
|
|
control.inspection_only = True
|
|
control.inspection_promotion_allowed = False
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
binding = _seed_supervised_connection(service)
|
|
control.verified_control = _verified_control_for_binding(binding)
|
|
service._acquire_application_control_process_lease() # noqa: SLF001
|
|
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", lambda _target: True)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
|
|
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 transitions == [
|
|
"inspection-closed",
|
|
"inspection-retired",
|
|
"canonical-opened",
|
|
]
|
|
assert state["application_control_session"]["state"] == "connection-ready"
|
|
assert state["application_control_session"]["inspection_only"] is False
|
|
assert state["application_control_session"]["session_generation"] == 8
|
|
service._release_application_control_process_lease() # noqa: SLF001
|
|
|
|
|
|
def test_reachable_connection_lease_supports_repeated_independent_control_sessions(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_supervised_connection(
|
|
service,
|
|
target_ipv4="10.255.254.54",
|
|
with_control=False,
|
|
)
|
|
service._device_session_id = "known-session" # noqa: SLF001
|
|
service._compatibility_attestation = ATTESTATION.model_dump(mode="json") # noqa: SLF001
|
|
opened_hosts: list[str] = []
|
|
|
|
class FakeCompletedControlSession:
|
|
def __init__(self) -> None:
|
|
self.state = "completed"
|
|
self.verified_control: dict[str, object] | None = None
|
|
|
|
def snapshot(self) -> dict[str, object]:
|
|
return {
|
|
"state": self.state,
|
|
"can_open": True,
|
|
"can_confirm_standby": False,
|
|
"verified_control": self.verified_control,
|
|
}
|
|
|
|
def open(
|
|
self,
|
|
*,
|
|
host: str,
|
|
connection_binding: ApplicationConnectionBinding,
|
|
**_: object,
|
|
) -> dict[str, object]:
|
|
opened_hosts.append(host)
|
|
self.state = "connection-ready"
|
|
self.verified_control = _verified_control_for_binding(
|
|
connection_binding,
|
|
control_session_id=f"fake-control-session-{len(opened_hosts)}",
|
|
)
|
|
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)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
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)
|
|
fake_session = service._application_control_session # noqa: SLF001
|
|
fake_session.state = "completed" # type: ignore[attr-defined]
|
|
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"]
|
|
== (state["connection_supervisor"]["lease"]["generation"])
|
|
)
|
|
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
|
|
)
|
|
service._release_application_control_process_lease() # noqa: SLF001
|
|
|
|
|
|
@pytest.mark.parametrize("continuity_change", ["host-epoch", "bridge-to-quick"])
|
|
def test_old_device_info_binding_cannot_regain_authority_after_connection_change(
|
|
tmp_path: Path,
|
|
continuity_change: str,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
old_binding = _seed_supervised_connection(
|
|
service,
|
|
target_ipv4=facade_module.AP_FALLBACK_IPV4,
|
|
connection_mode="bridge",
|
|
logical_device_id="known-k1",
|
|
with_control=False,
|
|
)
|
|
old_control_snapshot = {
|
|
"state": "connection-ready",
|
|
"verified_control": _verified_control_for_binding(old_binding),
|
|
}
|
|
|
|
service._reconcile_connection_supervisor( # noqa: SLF001
|
|
old_control_snapshot,
|
|
runtime.snapshot(),
|
|
)
|
|
assert service._connection_supervisor.snapshot().authority.control_allowed is True # noqa: SLF001
|
|
|
|
target = EndpointTarget(
|
|
facade_module.AP_FALLBACK_IPV4,
|
|
facade_module.CONTROL_MQTT_PORT,
|
|
)
|
|
if continuity_change == "host-epoch":
|
|
new_epoch = service._connection_supervisor.observe_host_path( # noqa: SLF001
|
|
HostPathProbeResult(
|
|
available=True,
|
|
fingerprint="test-route-after-wifi-reassociation",
|
|
interface="test1",
|
|
source_ipv4="192.168.56.3",
|
|
route_class="direct",
|
|
)
|
|
)
|
|
current_intent = service._connection_supervisor.snapshot().intent # noqa: SLF001
|
|
assert current_intent is not None
|
|
assert service._connection_supervisor.observe_endpoint( # noqa: SLF001
|
|
target=target,
|
|
intent_id=current_intent.intent_id,
|
|
host_path_epoch=new_epoch,
|
|
reachable=True,
|
|
)
|
|
else:
|
|
new_intent_id = "test-intent-quick-after-bridge"
|
|
service._connection_supervisor.set_intent( # noqa: SLF001
|
|
intent_id=new_intent_id,
|
|
requested_mode="quick-connect",
|
|
expected_device_id="known-k1",
|
|
)
|
|
assert service._connection_supervisor.observe_device_network_applied( # noqa: SLF001
|
|
intent_id=new_intent_id,
|
|
transport_ref="test-ble-transport",
|
|
connection_mode="quick-connect",
|
|
target=target,
|
|
source="ble-post-write-status",
|
|
)
|
|
epoch = service._connection_supervisor.snapshot().host_path.epoch # noqa: SLF001
|
|
assert service._connection_supervisor.observe_endpoint( # noqa: SLF001
|
|
target=target,
|
|
intent_id=new_intent_id,
|
|
host_path_epoch=epoch,
|
|
reachable=True,
|
|
)
|
|
|
|
# The old application worker may still publish its old DeviceInfo snapshot.
|
|
# Reconciliation must compare the immutable binding, never relabel it with
|
|
# the new intent/route merely because the same TCP address answers.
|
|
service._reconcile_connection_supervisor( # noqa: SLF001
|
|
old_control_snapshot,
|
|
runtime.snapshot(),
|
|
)
|
|
supervisor = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
assert supervisor.endpoint.tcp_state == "reachable"
|
|
assert supervisor.authority.control_allowed is False
|
|
assert supervisor.authority.acquisition_start_allowed is False
|
|
assert supervisor.device_identity.state != "verified"
|
|
assert supervisor.lease.state == "configured-unverified"
|
|
|
|
|
|
def test_application_command_binding_guard_rejects_changed_host_epoch(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(service)
|
|
monkeypatch.setattr(service, "_sample_host_path", _direct_host_path)
|
|
monkeypatch.setattr(
|
|
service._application_control_session, # noqa: SLF001
|
|
"snapshot",
|
|
lambda: {
|
|
"state": "connection-ready",
|
|
"verified_control": _verified_control_for_binding(
|
|
binding,
|
|
control_session_id="test-control-test-ble-transport",
|
|
),
|
|
},
|
|
)
|
|
|
|
service._validate_application_connection_binding(binding) # noqa: SLF001
|
|
service._connection_supervisor.observe_host_path( # noqa: SLF001
|
|
HostPathProbeResult(
|
|
available=True,
|
|
fingerprint="route-after-mac-wifi-change",
|
|
interface="test1",
|
|
source_ipv4="192.168.56.3",
|
|
route_class="direct",
|
|
)
|
|
)
|
|
|
|
with pytest.raises(ApplicationConnectionBindingLost) as caught:
|
|
service._validate_application_connection_binding(binding) # noqa: SLF001
|
|
|
|
assert caught.value.reason_code == "application-connection-binding-lost"
|
|
|
|
|
|
def test_explicit_stop_dispatch_waits_for_transient_local_lifecycle_holder(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(service)
|
|
monkeypatch.setattr(service, "_sample_host_path", _direct_host_path)
|
|
monkeypatch.setattr(
|
|
service._application_control_session, # noqa: SLF001
|
|
"snapshot",
|
|
lambda: {
|
|
"state": "scanning",
|
|
"verified_control": _verified_control_for_binding(binding),
|
|
},
|
|
)
|
|
assert service._k1_command_dispatch_gate.acquire(blocking=False) # noqa: SLF001
|
|
entered = threading.Event()
|
|
completed = threading.Event()
|
|
release_callbacks: list[Callable[[], None]] = []
|
|
failures: list[BaseException] = []
|
|
|
|
def acquire_stop_lease() -> None:
|
|
entered.set()
|
|
try:
|
|
release_callbacks.append(
|
|
service._acquire_application_dispatch_lease( # noqa: SLF001
|
|
binding,
|
|
lambda: False,
|
|
)
|
|
)
|
|
except BaseException as exc: # pragma: no branch - asserted below
|
|
failures.append(exc)
|
|
finally:
|
|
completed.set()
|
|
|
|
worker = threading.Thread(target=acquire_stop_lease)
|
|
worker.start()
|
|
assert entered.wait(1.0)
|
|
assert completed.wait(0.1) is False
|
|
|
|
service._k1_command_dispatch_gate.release() # noqa: SLF001
|
|
assert completed.wait(1.0)
|
|
worker.join(timeout=1.0)
|
|
|
|
assert failures == []
|
|
assert len(release_callbacks) == 1
|
|
release_callbacks[0]()
|
|
assert service._k1_command_dispatch_gate.acquire(blocking=False) # noqa: SLF001
|
|
service._k1_command_dispatch_gate.release() # noqa: SLF001
|
|
|
|
|
|
def test_explicit_stop_dispatch_contention_expires_without_crossing_publish_lease(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(service)
|
|
assert service._k1_command_dispatch_gate.acquire(blocking=False) # noqa: SLF001
|
|
expired = threading.Event()
|
|
completed = threading.Event()
|
|
failures: list[BaseException] = []
|
|
|
|
def acquire_stop_lease() -> None:
|
|
try:
|
|
service._acquire_application_dispatch_lease( # noqa: SLF001
|
|
binding,
|
|
expired.is_set,
|
|
)
|
|
except BaseException as exc: # pragma: no branch - asserted below
|
|
failures.append(exc)
|
|
finally:
|
|
completed.set()
|
|
|
|
worker = threading.Thread(target=acquire_stop_lease)
|
|
worker.start()
|
|
assert completed.wait(0.1) is False
|
|
expired.set()
|
|
assert completed.wait(1.0)
|
|
worker.join(timeout=1.0)
|
|
service._k1_command_dispatch_gate.release() # noqa: SLF001
|
|
|
|
assert len(failures) == 1
|
|
assert isinstance(failures[0], ApplicationConnectionBindingLost)
|
|
assert service._k1_command_dispatch_gate.acquire(blocking=False) # noqa: SLF001
|
|
service._k1_command_dispatch_gate.release() # noqa: SLF001
|
|
|
|
|
|
def test_application_command_guard_projects_new_remote_proof_before_admission(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(service)
|
|
monkeypatch.setattr(service, "_sample_host_path", _direct_host_path)
|
|
control_session_id = "test-control-test-ble-transport"
|
|
monkeypatch.setattr(
|
|
service._application_control_session, # noqa: SLF001
|
|
"snapshot",
|
|
lambda: {
|
|
"state": "connection-ready",
|
|
"verified_control": _verified_control_for_binding(
|
|
binding,
|
|
control_session_id=control_session_id,
|
|
control_proof_revision=2,
|
|
control_proof_source="mqtt-heartbeat",
|
|
),
|
|
},
|
|
)
|
|
|
|
service._validate_application_connection_binding(binding) # noqa: SLF001
|
|
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
assert supervisor.snapshot().authority.control_allowed is True
|
|
assert supervisor._control_proof_revision == 2 # noqa: SLF001
|
|
assert supervisor._control_proof_source == "mqtt-heartbeat" # noqa: SLF001
|
|
|
|
|
|
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)
|
|
_seed_supervised_connection(
|
|
service,
|
|
target_ipv4="10.255.254.54",
|
|
with_control=False,
|
|
)
|
|
service._device_session_id = "old-session" # noqa: SLF001
|
|
service._compatibility_attestation = ATTESTATION.model_dump(mode="json") # noqa: SLF001
|
|
opened_hosts: list[str] = []
|
|
status_calls: list[dict[str, object]] = []
|
|
|
|
class FakeOpenControlSession:
|
|
def __init__(self) -> None:
|
|
self.state = "idle"
|
|
self.verified_control: dict[str, object] | None = None
|
|
|
|
def snapshot(self) -> dict[str, object]:
|
|
return {
|
|
"state": self.state,
|
|
"can_confirm_standby": False,
|
|
"verified_control": self.verified_control,
|
|
}
|
|
|
|
def open(
|
|
self,
|
|
*,
|
|
host: str,
|
|
connection_binding: ApplicationConnectionBinding,
|
|
**_: object,
|
|
) -> dict[str, object]:
|
|
opened_hosts.append(host)
|
|
self.state = "connection-ready"
|
|
self.verified_control = _verified_control_for_binding(connection_binding)
|
|
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",
|
|
)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
|
|
async def scenario() -> dict[str, Any]:
|
|
service.bind_runtime_event_loop()
|
|
return await asyncio.to_thread(
|
|
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 = asyncio.run(scenario())
|
|
|
|
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"] == "reachable"
|
|
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)
|
|
_seed_supervised_connection(
|
|
service,
|
|
target_ipv4="10.255.254.54",
|
|
endpoint_reachable=False,
|
|
with_control=False,
|
|
)
|
|
service._device_session_id = "known-session" # noqa: SLF001
|
|
service._compatibility_attestation = ATTESTATION.model_dump(mode="json") # 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"] == "configured-unverified"
|
|
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_persists_read_only_recovery_before_rejecting_control(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_supervised_connection(
|
|
service,
|
|
target_ipv4="192.168.68.50",
|
|
host_path=_tunnel_host_path("192.168.68.50"),
|
|
endpoint_reachable=False,
|
|
endpoint_reason="connection_lease_host_route_mismatch",
|
|
with_control=False,
|
|
)
|
|
service._device_session_id = "known-session" # noqa: SLF001
|
|
service._compatibility_attestation = ATTESTATION.model_dump(mode="json") # noqa: SLF001
|
|
|
|
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", lambda _target: False)
|
|
monkeypatch.setattr(facade_module, "_host_route_class", lambda _target: "tunnel")
|
|
service._read_wifi_status_on_runtime_loop = ( # type: ignore[method-assign] # noqa: SLF001
|
|
lambda *_args, **_kwargs: _wifi_status_read("192.168.68.77")
|
|
)
|
|
|
|
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["k1_ip"] == "192.168.68.77"
|
|
assert state["semantic_topology_store"]["record"]["ipv4"] == "192.168.68.77"
|
|
assert state["device_session"]["device_session_id"] != "known-session"
|
|
assert state["connection_verification"]["lease_state"] == "configured-unverified"
|
|
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(
|
|
_prepare_request(
|
|
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 = _prepare_request(
|
|
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):
|
|
_prepare_request(
|
|
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 = _prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
ten_hours = _prepare_request(
|
|
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:
|
|
with pytest.raises(ValidationError, match="idempotency_key"):
|
|
ConnectRequest(
|
|
device_id="synthetic-device",
|
|
ssid="synthetic-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
assert (
|
|
_connect_request(
|
|
device_id="synthetic-device",
|
|
connection_mode="quick-connect",
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
).connection_mode
|
|
== "quick-connect"
|
|
)
|
|
assert (
|
|
_connect_request(
|
|
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"
|
|
)
|
|
bridge = _connect_request(
|
|
device_id="synthetic-device",
|
|
ssid="synthetic-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
assert bridge.allow_host_wifi_switch is False
|
|
assert (
|
|
_connect_request(
|
|
device_id="synthetic-device",
|
|
ssid="synthetic-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
allow_host_wifi_switch=True,
|
|
).allow_host_wifi_switch
|
|
is True
|
|
)
|
|
with pytest.raises(ValidationError, match="valid boolean"):
|
|
_connect_request(
|
|
device_id="synthetic-device",
|
|
ssid="synthetic-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
allow_host_wifi_switch="yes",
|
|
)
|
|
with pytest.raises(ValidationError, match="only for Bridge"):
|
|
_connect_request(
|
|
device_id="synthetic-device",
|
|
connection_mode="quick-connect",
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
allow_host_wifi_switch=True,
|
|
)
|
|
with pytest.raises(ValidationError):
|
|
_connect_request(
|
|
device_id="synthetic-device",
|
|
connection_mode="quick-connect",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
with pytest.raises(ValidationError, match="host Wi-Fi profile"):
|
|
_connect_request(
|
|
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"):
|
|
_connect_request(
|
|
device_id="synthetic-device",
|
|
ssid="🛰️" * 9,
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
with pytest.raises(ValidationError, match="64 UTF-8 bytes"):
|
|
_connect_request(
|
|
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):
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
mount_type="uav", # type: ignore[arg-type]
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
with pytest.raises(ValidationError):
|
|
_prepare_request(
|
|
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 = ATTESTATION.model_dump(mode="json") # noqa: SLF001
|
|
service.arm_application_control_shadow(
|
|
ShadowApplicationControlArmRequest(
|
|
operator_confirmed=True,
|
|
timezone_name="Europe/Moscow",
|
|
)
|
|
)
|
|
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
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(
|
|
_prepare_request(
|
|
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(_start_request(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"
|
|
|
|
|
|
@pytest.mark.parametrize("proof_first", [False, True])
|
|
def test_plugin_commanded_acquisition_keeps_start_and_stop_as_explicit_actions(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
proof_first: bool,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control = FakeInteractiveControlSession()
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
binding = _seed_supervised_connection(service)
|
|
service._compatibility_attestation = ATTESTATION.model_dump(mode="json") # noqa: SLF001
|
|
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
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(_start_request(acquisition_id=acquisition_id))
|
|
assert control.start_projects == []
|
|
|
|
pre_pcl_state = service.start_acquisition(
|
|
_start_request(
|
|
acquisition_id=acquisition_id,
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
)
|
|
pre_pcl_camera_streams = [
|
|
stream
|
|
for stream in pre_pcl_state["sensor_catalog"]["streams"]
|
|
if stream.get("semantic_channel_id") == "camera.preview.live"
|
|
]
|
|
assert pre_pcl_state["camera_preview"]["active_source_id"] is None
|
|
assert pre_pcl_state["camera_preview"]["delivery"] is None
|
|
assert pre_pcl_state["camera_preview"]["activation_admission"]["state"] == (
|
|
"waiting-for-first-authoritative-pcl"
|
|
)
|
|
assert all(stream["activation"]["selected"] is False for stream in pre_pcl_camera_streams)
|
|
assert all(stream["activation"]["controllable"] is False for stream in pre_pcl_camera_streams)
|
|
assert all(stream["delivery"] is None for stream in pre_pcl_camera_streams)
|
|
pre_pcl_camera = service.camera_preview.snapshot()
|
|
assert pre_pcl_camera["active_source_id"] is None
|
|
assert pre_pcl_camera["delivery"] is None
|
|
assert pre_pcl_camera["recording"]["active"] is False
|
|
assert pre_pcl_camera["recording"]["producer_alive"] is False
|
|
assert control.start_projects == ["TEST001"]
|
|
assert control.stop_calls == 0
|
|
assert control.start_projects == ["TEST001"]
|
|
start_operation_id = service._acquisition_start_operation_id # noqa: SLF001
|
|
assert isinstance(start_operation_id, str)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_active_acquisition_checkpoint_start_operation_id",
|
|
lambda **_kwargs: start_operation_id,
|
|
)
|
|
physical_proof = _exact_start_physical_proof(
|
|
operation_id=start_operation_id,
|
|
acquisition_id=acquisition_id,
|
|
binding=binding,
|
|
resolved=True,
|
|
)
|
|
runtime.mark_ready()
|
|
if proof_first:
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"snapshot",
|
|
lambda: physical_proof,
|
|
)
|
|
proof_without_frame = service.state()
|
|
assert proof_without_frame["acquisition"]["state"] == ("awaiting_external_start")
|
|
assert proof_without_frame["last_operation"]["status"] == "running"
|
|
else:
|
|
runtime.pcl_frames = 1
|
|
frame_before_canonical_start = service.state()
|
|
assert frame_before_canonical_start["acquisition"]["state"] == ("awaiting_external_start")
|
|
assert frame_before_canonical_start["last_operation"]["status"] == "running"
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"snapshot",
|
|
lambda: physical_proof,
|
|
)
|
|
runtime.pcl_frames = 1
|
|
acquiring = service.state()
|
|
assert acquiring["acquisition"]["state"] == "acquiring"
|
|
assert acquiring["last_operation"]["status"] == "succeeded"
|
|
assert acquiring["last_operation"]["stage_code"] == ("canonical-start-and-first-point-frame")
|
|
assert acquiring["last_operation"]["result"]["confirmation"] == (
|
|
"canonical-start-and-point-frame"
|
|
)
|
|
|
|
stopping = service.stop_acquisition(
|
|
_stop_request(
|
|
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"
|
|
# This legacy mapping fixture does not let the fake request_stop replace
|
|
# the START projection with a typed durable STOP head. Bypass only its
|
|
# terminal checkpoint seam; dedicated integration tests cover real v5
|
|
# STOP proof -> checkpoint CEASED ordering.
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_cease_active_acquisition_checkpoint_from_physical_head",
|
|
lambda **_kwargs: True,
|
|
)
|
|
# The production worker has also resolved the durable STOP ledger before
|
|
# publishing `completed`; this mapping-only fake still returns the old
|
|
# START document. Preserve this test's pre-checkpoint intent by omitting
|
|
# that stale mapping at the reducer boundary.
|
|
original_reconcile_acquisition = service._reconcile_acquisition # noqa: SLF001
|
|
def reconcile_legacy_completed(
|
|
runtime_snapshot: Mapping[str, Any],
|
|
camera_snapshot: Mapping[str, Any],
|
|
control_snapshot: Mapping[str, Any],
|
|
**kwargs: Any,
|
|
) -> None:
|
|
kwargs["physical_command_proof"] = None
|
|
original_reconcile_acquisition(
|
|
runtime_snapshot,
|
|
camera_snapshot,
|
|
control_snapshot,
|
|
**kwargs,
|
|
)
|
|
|
|
monkeypatch.setattr(service, "_reconcile_acquisition", reconcile_legacy_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
|
|
assert completed["message"] == (
|
|
"Приём и запись завершены. K1 остановлен и готов к новому запуску."
|
|
)
|
|
|
|
|
|
def test_terminal_prepared_start_without_publish_settles_and_stops_local_capture(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control = FakeInteractiveControlSession()
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
binding = _seed_supervised_connection(
|
|
service,
|
|
transport_ref="k1-start-zero-publish",
|
|
)
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="START_NONE",
|
|
host=binding.target_ipv4,
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = str(prepared["acquisition"]["acquisition_id"])
|
|
identity = PhysicalCommandIdentity(
|
|
vendor_device_id_sha256="a" * 64,
|
|
device_serial_sha256="b" * 64,
|
|
)
|
|
assert control.verified_control is not None
|
|
connection = PhysicalCommandConnectionBinding(
|
|
intent_id=binding.intent_id,
|
|
transport_ref=binding.transport_ref,
|
|
connection_mode=binding.connection_mode,
|
|
target_ipv4=binding.target_ipv4,
|
|
target_port=binding.target_port,
|
|
host_path_epoch=binding.host_path_epoch,
|
|
control_session_id=str(control.verified_control["control_session_id"]),
|
|
producer_generation=int(control.verified_control["producer_generation"]),
|
|
)
|
|
coordinator = service._physical_command_coordinator # noqa: SLF001
|
|
coordinator.application_response(
|
|
ApplicationMqttResponseEvidence(
|
|
operation_key="bootstrap:start-none:DeviceInfoRequest",
|
|
response_topic="lixel/application/response/device_info",
|
|
payload_sha256="c" * 64,
|
|
modeling_action=None,
|
|
result_code=None,
|
|
success=None,
|
|
observed_at_utc="2026-08-14T13:30:40.000Z",
|
|
)
|
|
)
|
|
coordinator.bind_control_session(
|
|
PhysicalCommandRuntimeBinding(
|
|
vendor_device_id_sha256=identity.vendor_device_id_sha256,
|
|
device_serial_sha256=identity.device_serial_sha256,
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
intent_id=connection.intent_id,
|
|
transport_ref=connection.transport_ref,
|
|
connection_mode=connection.connection_mode,
|
|
target_ipv4=connection.target_ipv4,
|
|
target_port=connection.target_port,
|
|
host_path_epoch=connection.host_path_epoch,
|
|
control_session_id=connection.control_session_id,
|
|
producer_generation=connection.producer_generation,
|
|
)
|
|
)
|
|
coordinator.device_status(
|
|
ApplicationMqttDeviceStatusEvidence(
|
|
vendor_device_id_sha256=identity.vendor_device_id_sha256,
|
|
device_serial_sha256=identity.device_serial_sha256,
|
|
session_state="ready",
|
|
session_state_code=MODELING_STATE_BASE + 300,
|
|
project_bound=False,
|
|
project_id_sha256=None,
|
|
init_ready=False,
|
|
status_message_sha256="d" * 64,
|
|
mqtt_retained=False,
|
|
observed_at_utc="2026-08-14T13:30:41.000Z",
|
|
)
|
|
)
|
|
payload = b"exact-start-zero-publish"
|
|
envelope = OneShotPublishEnvelope(
|
|
operation_key="modeling:start",
|
|
topic="lixel/application/request/modeling",
|
|
payload=payload,
|
|
payload_sha256=hashlib.sha256(payload).hexdigest(),
|
|
payload_bytes=len(payload),
|
|
qos=2,
|
|
retain=False,
|
|
)
|
|
|
|
def request_start_after_durable_prepare(**kwargs: object) -> dict[str, object]:
|
|
context = kwargs["command_context"]
|
|
observer = kwargs["preparation_checkpoint_observer"]
|
|
assert isinstance(context, PhysicalCommandIntentContext)
|
|
assert callable(observer)
|
|
coordinator.prepare(context, action="start", envelope=envelope)
|
|
observer("prepared", context, envelope)
|
|
control._accept_checkpoint( # noqa: SLF001
|
|
expected_session_generation=kwargs["expected_session_generation"], # type: ignore[arg-type]
|
|
expected_state_revision=kwargs["expected_state_revision"], # type: ignore[arg-type]
|
|
)
|
|
control.start_projects.append(str(kwargs["project_name"]))
|
|
control.start_contexts.append(context)
|
|
control.state = "start-requested"
|
|
return control.snapshot()
|
|
|
|
control.request_start = request_start_after_durable_prepare # type: ignore[method-assign]
|
|
starting = service.start_acquisition(
|
|
_start_request(
|
|
acquisition_id=acquisition_id,
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
)
|
|
start_operation_id = str(starting["last_operation"]["operation_id"])
|
|
assert starting["physical_command"]["record"]["stage"] == "prepared"
|
|
assert starting["active_acquisition_recovery_checkpoint"]["state"] == "prepared"
|
|
assert runtime.source_mode == "live"
|
|
|
|
control.state = "failed"
|
|
control.state_revision += 1
|
|
control.failure = {
|
|
"reason_code": "application-connection-binding-lost",
|
|
"failed_phase": "start-requested",
|
|
"modeling_command_attempted": False,
|
|
"diagnostic_evidence_unavailable": [],
|
|
"safe_to_retry": True,
|
|
}
|
|
control.outcome_unknown = False
|
|
settled = service.state()
|
|
|
|
record = service._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
checkpoint = service._active_acquisition_checkpoint.snapshot().checkpoint # type: ignore[union-attr] # noqa: SLF001
|
|
operation = service._operations.get(start_operation_id) # noqa: SLF001
|
|
assert record is not None
|
|
assert record.stage == "resolved"
|
|
assert record.resolution == "not-dispatched"
|
|
assert checkpoint is not None and checkpoint.state == "ceased"
|
|
assert settled["acquisition"]["state"] == "failed"
|
|
assert settled["acquisition"]["result"]["device_start"] == "not-dispatched"
|
|
assert settled["acquisition"]["result"]["receiver_stopped"] is True
|
|
assert operation.status == "failed"
|
|
assert operation.stage_code == "physical-start-not-dispatched"
|
|
assert operation.error is not None
|
|
assert operation.error["side_effect_status"] == "none"
|
|
assert operation.error["physical_command_sent"] is False
|
|
assert runtime.stop_calls == 1
|
|
assert runtime.source_mode == "idle"
|
|
assert service._acquisition_session_lease is None # noqa: SLF001
|
|
|
|
|
|
def test_live_prepared_start_reduction_does_not_touch_publish_fence(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
acquisition_id = "acq-live-prepared"
|
|
operation_id = "op-live-prepared"
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"snapshot",
|
|
lambda: {
|
|
"status": "unresolved",
|
|
"record": {
|
|
"operation_id": operation_id,
|
|
"acquisition_id": acquisition_id,
|
|
"action": "start",
|
|
"stage": "prepared",
|
|
"resolution": None,
|
|
"publish_call_returned": None,
|
|
"packet_id": None,
|
|
"qos2_completed": False,
|
|
"application_response": None,
|
|
},
|
|
},
|
|
)
|
|
monkeypatch.setattr(
|
|
service._application_control_session, # noqa: SLF001
|
|
"snapshot",
|
|
lambda: {"state": "initializing", "failure": None},
|
|
)
|
|
|
|
class PublishFenceMustStayFree:
|
|
def acquire(self, *args: object, **kwargs: object) -> bool:
|
|
raise AssertionError("live START reduction touched the publish fence")
|
|
|
|
def release(self) -> None:
|
|
raise AssertionError("live START reduction released an unowned fence")
|
|
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_k1_command_dispatch_gate",
|
|
PublishFenceMustStayFree(),
|
|
)
|
|
|
|
outcome = service._settle_prepared_start_worker_failure( # noqa: SLF001
|
|
acquisition_id=acquisition_id,
|
|
start_operation_id=operation_id,
|
|
)
|
|
|
|
assert outcome == "not-applicable"
|
|
|
|
|
|
def _activate_real_checkpoint_for_prepared_stop_fixture(
|
|
service: XgridsK1CompatibilityService,
|
|
*,
|
|
project_name: str,
|
|
) -> None:
|
|
"""Back the specialized retained-receiver fixture with the real v5 CAS."""
|
|
|
|
store = service._active_acquisition_checkpoint # noqa: SLF001
|
|
acquisition = service._acquisition # noqa: SLF001
|
|
out_dir = service._acquisition_out_dir # noqa: SLF001
|
|
record = service._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
assert store is not None
|
|
assert acquisition is not None
|
|
assert out_dir is not None
|
|
assert record is not None and record.last_status is not None
|
|
binding = ActiveAcquisitionRecoveryTransportBinding(
|
|
runtime_instance_id=service._snapshot_runtime_id, # noqa: SLF001
|
|
intent_id=record.connection.intent_id,
|
|
transport_ref=record.connection.transport_ref,
|
|
connection_mode=record.connection.connection_mode,
|
|
target_ipv4=record.connection.target_ipv4,
|
|
target_port=record.connection.target_port,
|
|
host_path_epoch=record.connection.host_path_epoch,
|
|
control_session_id=record.connection.control_session_id,
|
|
producer_generation=record.connection.producer_generation,
|
|
logical_device_id=acquisition.device_id,
|
|
compatibility_profile_id=record.compatibility_profile_id,
|
|
vendor_device_id_sha256=record.identity.vendor_device_id_sha256,
|
|
device_serial_sha256=record.identity.device_serial_sha256,
|
|
)
|
|
prepared = store.prepare(
|
|
transition_id=f"test-retained-prepare:{record.operation_id}",
|
|
predecessor_revision=0,
|
|
acquisition_id=acquisition.acquisition_id,
|
|
original_start_operation_id=record.operation_id,
|
|
start_payload_sha256=record.payload_sha256,
|
|
identity=ActiveAcquisitionRecoveryIdentity(
|
|
logical_device_id=acquisition.device_id,
|
|
vendor_device_id_sha256=record.identity.vendor_device_id_sha256,
|
|
device_serial_sha256=record.identity.device_serial_sha256,
|
|
),
|
|
connection=ActiveAcquisitionRecoveryConnection(
|
|
transport_ref=record.connection.transport_ref,
|
|
connection_mode=record.connection.connection_mode,
|
|
target_ipv4=record.connection.target_ipv4,
|
|
target_port=record.connection.target_port,
|
|
),
|
|
compatibility_profile_id=record.compatibility_profile_id,
|
|
project_name=project_name,
|
|
project_name_wire_sha256=active_acquisition_project_name_sha256(
|
|
project_name
|
|
),
|
|
original_evidence_session_id=out_dir.name,
|
|
duration_seconds=acquisition.duration_seconds,
|
|
requested_streams=acquisition.requested_streams,
|
|
evidence_policy=acquisition.evidence_policy,
|
|
mount_type="handheld",
|
|
gnss_mode="none",
|
|
prepared_binding=binding,
|
|
)
|
|
status_proof = service._checkpoint_status_proof( # noqa: SLF001
|
|
status=record.last_status,
|
|
binding=binding,
|
|
evidence_session_id=out_dir.name,
|
|
)
|
|
store.activate(
|
|
transition_id=f"test-retained-activate:{record.operation_id}",
|
|
expected_revision=prepared.revision,
|
|
expected_acquisition_id=prepared.acquisition_id,
|
|
expected_start_operation_id=prepared.original_start_operation_id,
|
|
status_proof=status_proof,
|
|
physical_proof=service._checkpoint_physical_proof( # noqa: SLF001
|
|
record=record,
|
|
binding=binding,
|
|
checkpoint=prepared,
|
|
),
|
|
)
|
|
|
|
|
|
def _install_real_prepared_stop_dispatch_fixture(
|
|
service: XgridsK1CompatibilityService,
|
|
runtime: FakeVisualizationRuntime,
|
|
*,
|
|
confirm_first_pcl: bool = True,
|
|
) -> SimpleNamespace:
|
|
"""Install one fully confirmed START and a STOP that only durably PREPAREs."""
|
|
|
|
control = FakeInteractiveControlSession()
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
binding = _seed_supervised_connection(service, transport_ref="k1-stop-dispatch-race")
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="STOP_RACE",
|
|
host=binding.target_ipv4,
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = str(prepared["acquisition"]["acquisition_id"])
|
|
service.start_acquisition(
|
|
_start_request(
|
|
acquisition_id=acquisition_id,
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
)
|
|
start_operation_id = service._acquisition_start_operation_id # noqa: SLF001
|
|
assert isinstance(start_operation_id, str)
|
|
|
|
identity = PhysicalCommandIdentity(
|
|
vendor_device_id_sha256="a" * 64,
|
|
device_serial_sha256="b" * 64,
|
|
)
|
|
connection = PhysicalCommandConnectionBinding(
|
|
intent_id=binding.intent_id,
|
|
transport_ref=binding.transport_ref,
|
|
connection_mode=binding.connection_mode,
|
|
target_ipv4=binding.target_ipv4,
|
|
target_port=binding.target_port,
|
|
host_path_epoch=binding.host_path_epoch,
|
|
control_session_id="fake-control-session",
|
|
producer_generation=1,
|
|
)
|
|
|
|
def status(session_state: str, observed_at_utc: str) -> PhysicalCommandStatusEvidence:
|
|
scanning = session_state == "scanning"
|
|
return PhysicalCommandStatusEvidence(
|
|
source="live-control-session",
|
|
vendor_device_id_sha256=identity.vendor_device_id_sha256,
|
|
device_serial_sha256=identity.device_serial_sha256,
|
|
control_session_id=connection.control_session_id,
|
|
host_path_epoch=connection.host_path_epoch,
|
|
producer_generation=connection.producer_generation,
|
|
session_state=session_state, # type: ignore[arg-type]
|
|
session_state_code=302 if scanning else 300,
|
|
project_bound=scanning,
|
|
project_id_sha256="c" * 64 if scanning else None,
|
|
init_ready=scanning,
|
|
status_message_sha256=hashlib.sha256(observed_at_utc.encode()).hexdigest(),
|
|
mqtt_retained=False,
|
|
observed_at_utc=observed_at_utc,
|
|
)
|
|
|
|
ledger = service._physical_command_ledger # noqa: SLF001
|
|
ledger.prepare(
|
|
operation_id=start_operation_id,
|
|
parent_operation_id=None,
|
|
acquisition_id=acquisition_id,
|
|
action="start",
|
|
identity=identity,
|
|
connection=connection,
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
payload_sha256="1" * 64,
|
|
baseline_status=status("ready", "2026-08-12T08:00:00.000Z"),
|
|
)
|
|
ledger.mark_dispatching(start_operation_id)
|
|
ledger.mark_observing(start_operation_id, publish_call_returned=True, packet_id=71)
|
|
ledger.mark_qos2_completed(start_operation_id, packet_id=71)
|
|
ledger.record_application_response(
|
|
start_operation_id,
|
|
PhysicalCommandApplicationResponse(
|
|
operation_id=start_operation_id,
|
|
action="start",
|
|
control_session_id=connection.control_session_id,
|
|
host_path_epoch=connection.host_path_epoch,
|
|
producer_generation=connection.producer_generation,
|
|
result_code=PHYSICAL_COMMAND_APPLICATION_SUCCESS_CODE,
|
|
success=True,
|
|
payload_sha256="2" * 64,
|
|
observed_at_utc="2026-08-12T08:00:01.000Z",
|
|
),
|
|
)
|
|
ledger.record_status_observation(
|
|
start_operation_id,
|
|
status("scanning", "2026-08-12T08:00:02.000Z"),
|
|
)
|
|
ledger.resolve(start_operation_id, resolution="start-active-observed")
|
|
|
|
coordinator = service._physical_command_coordinator # noqa: SLF001
|
|
coordinator.application_response(
|
|
ApplicationMqttResponseEvidence(
|
|
operation_key="bootstrap:stop-race:DeviceInfoRequest",
|
|
response_topic="lixel/application/response/device_info",
|
|
payload_sha256="3" * 64,
|
|
modeling_action=None,
|
|
result_code=None,
|
|
success=None,
|
|
observed_at_utc="2026-08-12T08:00:03.000Z",
|
|
)
|
|
)
|
|
coordinator.bind_control_session(
|
|
PhysicalCommandRuntimeBinding(
|
|
vendor_device_id_sha256=identity.vendor_device_id_sha256,
|
|
device_serial_sha256=identity.device_serial_sha256,
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
intent_id=connection.intent_id,
|
|
transport_ref=connection.transport_ref,
|
|
connection_mode=connection.connection_mode,
|
|
target_ipv4=connection.target_ipv4,
|
|
target_port=connection.target_port,
|
|
host_path_epoch=connection.host_path_epoch,
|
|
control_session_id=connection.control_session_id,
|
|
producer_generation=connection.producer_generation,
|
|
)
|
|
)
|
|
coordinator.device_status(
|
|
ApplicationMqttDeviceStatusEvidence(
|
|
vendor_device_id_sha256=identity.vendor_device_id_sha256,
|
|
device_serial_sha256=identity.device_serial_sha256,
|
|
session_state="scanning",
|
|
session_state_code=MODELING_STATE_BASE + 302,
|
|
project_bound=True,
|
|
project_id_sha256="c" * 64,
|
|
init_ready=True,
|
|
status_message_sha256="4" * 64,
|
|
mqtt_retained=False,
|
|
observed_at_utc="2026-08-12T08:00:04.000Z",
|
|
)
|
|
)
|
|
_activate_real_checkpoint_for_prepared_stop_fixture(
|
|
service,
|
|
project_name="STOP_RACE",
|
|
)
|
|
runtime.mark_ready()
|
|
runtime.pcl_frames = 1 if confirm_first_pcl else 0
|
|
assert service.state()["acquisition"]["state"] == (
|
|
"acquiring" if confirm_first_pcl else "awaiting_external_start"
|
|
)
|
|
|
|
payload = b"exact-stop-dispatch-race"
|
|
envelope = OneShotPublishEnvelope(
|
|
operation_key="modeling:stop",
|
|
topic="lixel/application/request/modeling",
|
|
payload=payload,
|
|
payload_sha256=hashlib.sha256(payload).hexdigest(),
|
|
payload_bytes=len(payload),
|
|
qos=2,
|
|
retain=False,
|
|
)
|
|
original_request_stop = control.request_stop
|
|
prepare_calls: list[str] = []
|
|
|
|
def request_stop_after_durable_prepare(**kwargs: object) -> dict[str, object]:
|
|
command_context = kwargs["command_context"]
|
|
coordinator.prepare(command_context, action="stop", envelope=envelope) # type: ignore[arg-type]
|
|
prepare_calls.append(str(command_context.operation_id)) # type: ignore[attr-defined]
|
|
result = original_request_stop(**kwargs) # type: ignore[arg-type]
|
|
control.state = "stop-requested"
|
|
return result
|
|
|
|
control.request_stop = request_stop_after_durable_prepare # type: ignore[method-assign]
|
|
stop_operation_id = "op-00000000-0000-4000-8000-000000001401"
|
|
request = _stop_request(
|
|
acquisition_id=acquisition_id,
|
|
operation_id=stop_operation_id,
|
|
idempotency_key="prepared-stop-dispatch-race",
|
|
mode="graceful",
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
return SimpleNamespace(
|
|
service=service,
|
|
runtime=runtime,
|
|
control=control,
|
|
binding=binding,
|
|
coordinator=coordinator,
|
|
ledger=ledger,
|
|
identity=identity,
|
|
connection=connection,
|
|
status=status,
|
|
envelope=envelope,
|
|
request=request,
|
|
start_operation_id=start_operation_id,
|
|
stop_operation_id=stop_operation_id,
|
|
prepare_calls=prepare_calls,
|
|
)
|
|
|
|
|
|
def _observe_stop_race_route_loss(service: XgridsK1CompatibilityService) -> None:
|
|
service._connection_supervisor.observe_host_path( # noqa: SLF001
|
|
HostPathProbeResult(
|
|
available=False,
|
|
fingerprint=None,
|
|
interface=None,
|
|
source_ipv4=None,
|
|
route_class="unavailable",
|
|
reason_code="router-link-lost",
|
|
)
|
|
)
|
|
|
|
|
|
def _settle_real_prepared_stop_before_publish(
|
|
fixture: SimpleNamespace,
|
|
) -> facade_module._PreparedStopRecoveryOwner:
|
|
"""Drive the real S0 row to truthful NONE while retaining its receiver."""
|
|
|
|
fixture.control.state = "failed"
|
|
fixture.control.state_revision += 1
|
|
fixture.control.failure = {
|
|
"reason_code": "application-connection-binding-lost",
|
|
"modeling_command_attempted": False,
|
|
"safe_to_retry": False,
|
|
}
|
|
fixture.service.state()
|
|
owner = fixture.service._prepared_stop_recovery_owner # noqa: SLF001
|
|
assert isinstance(owner, facade_module._PreparedStopRecoveryOwner)
|
|
operation = fixture.service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.stage_code == "physical-stop-classified-not-dispatched"
|
|
assert operation.error is not None
|
|
assert operation.error["side_effect_status"] == "none"
|
|
return owner
|
|
|
|
|
|
def _classify_real_prepared_stop(
|
|
fixture: SimpleNamespace,
|
|
owner: facade_module._PreparedStopRecoveryOwner,
|
|
*,
|
|
session_state: str,
|
|
reconciliation_id: str,
|
|
) -> dict[str, object]:
|
|
"""Install a fresh inspection binding and classify S0 with real coordinator audit."""
|
|
|
|
scanning = session_state == "scanning"
|
|
runtime_binding = PhysicalCommandRuntimeBinding(
|
|
vendor_device_id_sha256="a" * 64,
|
|
device_serial_sha256="b" * 64,
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
intent_id=fixture.binding.intent_id,
|
|
transport_ref=fixture.binding.transport_ref,
|
|
connection_mode=fixture.binding.connection_mode,
|
|
target_ipv4=fixture.binding.target_ipv4,
|
|
target_port=fixture.binding.target_port,
|
|
host_path_epoch=fixture.binding.host_path_epoch + 1,
|
|
control_session_id=f"fresh-{reconciliation_id}",
|
|
producer_generation=2,
|
|
)
|
|
fixture.coordinator.prepare_read_only_bootstrap()
|
|
fixture.coordinator.application_response(
|
|
ApplicationMqttResponseEvidence(
|
|
operation_key=f"bootstrap:{reconciliation_id}:DeviceInfoRequest",
|
|
response_topic="lixel/application/response/device_info",
|
|
payload_sha256="f" * 64,
|
|
modeling_action=None,
|
|
result_code=None,
|
|
success=None,
|
|
observed_at_utc="2026-08-12T08:00:59.000Z",
|
|
)
|
|
)
|
|
fixture.coordinator.bind_control_session(runtime_binding)
|
|
fixture.coordinator.device_status(
|
|
ApplicationMqttDeviceStatusEvidence(
|
|
vendor_device_id_sha256=runtime_binding.vendor_device_id_sha256,
|
|
device_serial_sha256=runtime_binding.device_serial_sha256,
|
|
session_state=session_state, # type: ignore[arg-type]
|
|
session_state_code=MODELING_STATE_BASE + (302 if scanning else 300),
|
|
project_bound=scanning,
|
|
project_id_sha256="c" * 64 if scanning else None,
|
|
init_ready=scanning,
|
|
status_message_sha256="e" * 64,
|
|
mqtt_retained=False,
|
|
observed_at_utc="2026-08-12T08:01:00.000Z",
|
|
)
|
|
)
|
|
return dict(
|
|
fixture.service._classify_retained_prepared_stop_owned( # noqa: SLF001
|
|
owner=owner,
|
|
reconciliation_id=reconciliation_id,
|
|
reconcile=lambda: fixture.coordinator.reconcile_unresolved(
|
|
reconciliation_id=reconciliation_id,
|
|
),
|
|
)
|
|
)
|
|
|
|
|
|
def _resolve_real_stop_ready(fixture: SimpleNamespace) -> None:
|
|
"""Advance the fixture's exact S0 to canonical durable READY without polling."""
|
|
|
|
fixture.ledger.mark_dispatching(fixture.stop_operation_id)
|
|
fixture.ledger.mark_observing(
|
|
fixture.stop_operation_id,
|
|
publish_call_returned=True,
|
|
packet_id=72,
|
|
)
|
|
fixture.ledger.mark_qos2_completed(fixture.stop_operation_id, packet_id=72)
|
|
fixture.ledger.record_application_response(
|
|
fixture.stop_operation_id,
|
|
PhysicalCommandApplicationResponse(
|
|
operation_id=fixture.stop_operation_id,
|
|
action="stop",
|
|
control_session_id=fixture.connection.control_session_id,
|
|
host_path_epoch=fixture.connection.host_path_epoch,
|
|
producer_generation=fixture.connection.producer_generation,
|
|
result_code=PHYSICAL_COMMAND_APPLICATION_SUCCESS_CODE,
|
|
success=True,
|
|
payload_sha256="5" * 64,
|
|
observed_at_utc="2026-08-12T08:00:05.000Z",
|
|
),
|
|
)
|
|
fixture.ledger.record_status_observation(
|
|
fixture.stop_operation_id,
|
|
fixture.status("ready", "2026-08-12T08:00:06.000Z"),
|
|
)
|
|
fixture.ledger.resolve(
|
|
fixture.stop_operation_id,
|
|
resolution="stop-standby-observed",
|
|
)
|
|
|
|
|
|
def test_prepared_stop_and_dispatch_gate_fence_route_loss_without_duplicate_publish(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
|
|
stopping = service.stop_acquisition(fixture.request)
|
|
prepared_record = fixture.ledger.snapshot().record
|
|
assert prepared_record is not None
|
|
assert prepared_record.operation_id == fixture.stop_operation_id
|
|
assert prepared_record.stage == "prepared"
|
|
assert service._prepared_stop_dispatch_lineage_is_current() is True # noqa: SLF001
|
|
assert stopping["selected_device_id"] == fixture.binding.transport_ref
|
|
|
|
# A route-negative state reduction lands after durable PREPARE but before
|
|
# the asynchronous worker owns the command-dispatch gate. It must not
|
|
# tear down the only topology that can execute this already accepted STOP.
|
|
for _ in range(3):
|
|
_observe_stop_race_route_loss(service)
|
|
state = service.state()
|
|
assert state["selected_device_id"] == fixture.binding.transport_ref
|
|
assert fixture.prepare_calls == [fixture.stop_operation_id]
|
|
repeated = service.stop_acquisition(fixture.request)
|
|
assert repeated["selected_device_id"] == fixture.binding.transport_ref
|
|
assert fixture.prepare_calls == [fixture.stop_operation_id]
|
|
|
|
dispatch = ApplicationMqttPublishEvidence(
|
|
operation_key="modeling:stop",
|
|
topic=fixture.envelope.topic,
|
|
payload_sha256=fixture.envelope.payload_sha256,
|
|
qos=2,
|
|
retain=False,
|
|
packet_id=None,
|
|
)
|
|
published: list[str] = []
|
|
assert service._k1_command_dispatch_gate.acquire(blocking=False) # noqa: SLF001
|
|
try:
|
|
fixture.coordinator.publish_dispatching(dispatch)
|
|
dispatching = fixture.ledger.snapshot().record
|
|
assert dispatching is not None and dispatching.stage == "dispatching"
|
|
# This is the exact mark_dispatching -> client.publish interleaving.
|
|
# The reducer sees confirmed loss but cannot cross the transport gate.
|
|
mid_dispatch = service.state()
|
|
assert mid_dispatch["selected_device_id"] == fixture.binding.transport_ref
|
|
published.append(fixture.envelope.payload_sha256)
|
|
finally:
|
|
service._k1_command_dispatch_gate.release() # noqa: SLF001
|
|
|
|
assert published == [fixture.envelope.payload_sha256]
|
|
assert fixture.prepare_calls == [fixture.stop_operation_id]
|
|
|
|
# Once the dispatch fence is released, the same genuine route loss uses
|
|
# the normal fail-closed reducer; PREPARED lineage no longer applies.
|
|
retired = service.state()
|
|
assert retired["selected_device_id"] is None
|
|
assert retired["device_session"] is None
|
|
|
|
|
|
@pytest.mark.parametrize("terminal_reason", ["control-failed", "deadline"])
|
|
def test_prepared_stop_barrier_expires_and_real_loss_retires_fail_closed(
|
|
tmp_path: Path,
|
|
terminal_reason: str,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
|
|
if terminal_reason == "control-failed":
|
|
fixture.control.state = "failed"
|
|
fixture.control.state_revision += 1
|
|
fixture.control.failure = {
|
|
"reason_code": "mqtt_network_loop_failed",
|
|
"modeling_command_attempted": False,
|
|
"safe_to_retry": False,
|
|
}
|
|
else:
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
operation.deadline_at = datetime.now(UTC) - timedelta(seconds=1)
|
|
for _ in range(3):
|
|
_observe_stop_race_route_loss(service)
|
|
|
|
assert service._prepared_stop_dispatch_lineage_is_current() is False # noqa: SLF001
|
|
retired = service.state()
|
|
if terminal_reason == "control-failed":
|
|
# A terminal pre-publish worker is no longer merely an expired dispatch
|
|
# barrier: it is exact NONE proof and retains the receiver/topology for
|
|
# the automatic read-only SCANNING/READY classification path.
|
|
assert retired["selected_device_id"] == fixture.binding.transport_ref
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.stage_code == "physical-stop-classified-not-dispatched"
|
|
assert operation.error is not None
|
|
assert operation.error["side_effect_status"] == "none"
|
|
assert service._prepared_stop_recovery_owner is not None # noqa: SLF001
|
|
else:
|
|
# A hung nonterminal worker whose bounded operation deadline elapsed has
|
|
# no terminal NONE proof. Genuine route loss therefore retires normally.
|
|
assert retired["selected_device_id"] is None
|
|
assert fixture.ledger.snapshot().record is not None
|
|
assert fixture.ledger.snapshot().record.stage == "prepared" # type: ignore[union-attr]
|
|
assert fixture.prepare_calls == [fixture.stop_operation_id]
|
|
|
|
|
|
def test_prepared_stop_pending_worker_outranks_runtime_error_then_settles_none(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
runtime.phase = "error"
|
|
|
|
# Local producer symptoms cannot terminalize an accepted STOP while the
|
|
# exact durable command is still PREPARED and its worker is nonterminal.
|
|
pending = service.state()
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert pending["acquisition"]["state"] == "awaiting_external_stop"
|
|
assert operation.status == "running"
|
|
assert operation.error is None
|
|
assert fixture.ledger.snapshot().record is not None
|
|
assert fixture.ledger.snapshot().record.stage == "prepared" # type: ignore[union-attr]
|
|
|
|
# The same row plus a terminal pre-publish worker is exact NONE proof.
|
|
fixture.control.state = "failed"
|
|
fixture.control.state_revision += 1
|
|
fixture.control.failure = {
|
|
"reason_code": "application-connection-binding-lost",
|
|
"modeling_command_attempted": False,
|
|
"safe_to_retry": False,
|
|
}
|
|
settled = service.state()
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.status == "failed"
|
|
assert operation.stage_code == "physical-stop-classified-not-dispatched"
|
|
assert operation.error is not None
|
|
assert operation.error["side_effect_status"] == "none"
|
|
assert operation.error["physical_command_sent"] is False
|
|
assert settled["selected_device_id"] is None
|
|
|
|
|
|
def test_expired_stop_dispatch_admission_resolves_durable_no_dispatch(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
assert len(fixture.control.stop_dispatch_deadlines) == 1
|
|
deadline_reached = fixture.control.stop_dispatch_deadlines[0]
|
|
assert deadline_reached is not None
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
operation.deadline_at = datetime.now(UTC) - timedelta(seconds=1)
|
|
assert deadline_reached() is True
|
|
|
|
fixture.control.state = "failed"
|
|
fixture.control.state_revision += 1
|
|
fixture.control.failure = {
|
|
"reason_code": "physical-command-dispatch-deadline-expired",
|
|
"modeling_command_attempted": True,
|
|
"stop_command_attempted": False,
|
|
"stop_publish_attempts": 0,
|
|
"diagnostic_evidence_unavailable": [],
|
|
"safe_to_retry": True,
|
|
}
|
|
fixture.control.outcome_unknown = False
|
|
fixture.control.transport_publish_attempts = 8
|
|
|
|
settled = service.state()
|
|
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None
|
|
assert record.stage == "resolved"
|
|
assert record.resolution == "not-dispatched"
|
|
assert record.resolved_unclassified_stop_recovery_required is True
|
|
assert fixture.coordinator.snapshot()["active_operation_id"] is None
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.stage_code == "physical-stop-classified-not-dispatched"
|
|
assert operation.error is not None
|
|
assert operation.error["side_effect_status"] == "none"
|
|
assert operation.error["physical_command_sent"] is False
|
|
assert operation.error["automatic_replay_allowed"] is False
|
|
assert service.camera_preview.snapshot()["recording"]["source_end_expected"] is False
|
|
assert service._prepared_stop_recovery_owner is not None # noqa: SLF001
|
|
assert settled["physical_command"]["record"]["resolution"] == "not-dispatched"
|
|
|
|
|
|
def test_abort_rejects_exact_unresolved_stop_before_any_local_cleanup(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
before_operations = tuple(service._operations.snapshot()) # noqa: SLF001
|
|
|
|
with pytest.raises(
|
|
facade_module.LocalAcquisitionLifecycleError,
|
|
match="исход PREPARED/DISPATCHING STOP",
|
|
) as raised:
|
|
service.abort_acquisition(
|
|
_abort_request(acquisition_id=fixture.request.acquisition_id)
|
|
)
|
|
|
|
assert raised.value.reason_code == "acquisition-abort-physical-stop-unresolved"
|
|
assert tuple(service._operations.snapshot()) == before_operations # noqa: SLF001
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.status == "running"
|
|
assert operation.error is None
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None and record.stage == "prepared"
|
|
assert service._acquisition is not None # noqa: SLF001
|
|
assert service._acquisition.state == "awaiting_external_stop" # noqa: SLF001
|
|
assert service._acquisition_stop_operation_id == fixture.stop_operation_id # noqa: SLF001
|
|
assert runtime.stop_calls == 0
|
|
|
|
|
|
def test_capture_only_rejects_live_prepared_stop_before_operation_or_local_cleanup(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
acquisition = service._acquisition # noqa: SLF001
|
|
assert acquisition is not None
|
|
before_operations = tuple(service._operations.snapshot()) # noqa: SLF001
|
|
before_acquisition = (
|
|
acquisition.state,
|
|
acquisition.state_revision,
|
|
acquisition.message_code,
|
|
acquisition.result,
|
|
)
|
|
|
|
with pytest.raises(
|
|
facade_module.LocalAcquisitionLifecycleError,
|
|
match="ещё владеет PREPARED STOP",
|
|
) as raised:
|
|
service.stop_acquisition(
|
|
_stop_request(
|
|
acquisition_id=acquisition.acquisition_id,
|
|
operation_id="op-00000000-0000-4000-8000-000000001450",
|
|
mode="capture-only",
|
|
)
|
|
)
|
|
|
|
assert raised.value.reason_code == "acquisition-stop-worker-retirement-pending"
|
|
assert tuple(service._operations.snapshot()) == before_operations # noqa: SLF001
|
|
assert fixture.control.state == "stop-requested"
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None and record.stage == "prepared"
|
|
assert fixture.prepare_calls == [fixture.stop_operation_id]
|
|
assert (
|
|
acquisition.state,
|
|
acquisition.state_revision,
|
|
acquisition.message_code,
|
|
acquisition.result,
|
|
) == before_acquisition
|
|
assert service._acquisition_stop_operation_id == fixture.stop_operation_id # noqa: SLF001
|
|
assert runtime.stop_calls == 0
|
|
|
|
|
|
def test_capture_only_defers_terminal_prepared_stop_while_dispatch_gate_is_busy(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
fixture.control.state = "failed"
|
|
fixture.control.failure = {
|
|
"reason_code": "application-connection-binding-lost",
|
|
"modeling_command_attempted": False,
|
|
"safe_to_retry": False,
|
|
}
|
|
before_operations = tuple(service._operations.snapshot()) # noqa: SLF001
|
|
|
|
assert service._k1_command_dispatch_gate.acquire(blocking=False) # noqa: SLF001
|
|
try:
|
|
with pytest.raises(
|
|
facade_module.LocalAcquisitionLifecycleError,
|
|
match="publish-переходом STOP",
|
|
) as raised:
|
|
service.stop_acquisition(
|
|
_stop_request(
|
|
acquisition_id=fixture.request.acquisition_id,
|
|
operation_id="op-00000000-0000-4000-8000-000000001451",
|
|
mode="capture-only",
|
|
)
|
|
)
|
|
finally:
|
|
service._k1_command_dispatch_gate.release() # noqa: SLF001
|
|
|
|
assert raised.value.reason_code == "acquisition-stop-dispatch-retirement-pending"
|
|
assert tuple(service._operations.snapshot()) == before_operations # noqa: SLF001
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None and record.stage == "prepared"
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.status == "running"
|
|
assert operation.error is None
|
|
assert fixture.prepare_calls == [fixture.stop_operation_id]
|
|
assert runtime.stop_calls == 0
|
|
|
|
|
|
def test_capture_only_settles_terminal_prepared_stop_none_before_local_cleanup(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
fixture.control.state = "failed"
|
|
fixture.control.failure = {
|
|
"reason_code": "application-connection-binding-lost",
|
|
"modeling_command_attempted": False,
|
|
"safe_to_retry": False,
|
|
}
|
|
monkeypatch.setattr(service, "_seal_acquisition_capture_clock", lambda: None)
|
|
|
|
completed = service.stop_acquisition(
|
|
_stop_request(
|
|
acquisition_id=fixture.request.acquisition_id,
|
|
operation_id="op-00000000-0000-4000-8000-000000001452",
|
|
mode="capture-only",
|
|
)
|
|
)
|
|
|
|
original_stop = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert original_stop.status == "failed"
|
|
assert original_stop.stage_code == "physical-stop-classified-not-dispatched"
|
|
assert original_stop.error is not None
|
|
assert original_stop.error["side_effect_status"] == "none"
|
|
assert original_stop.error["physical_command_sent"] is False
|
|
local_stop = completed["last_operation"]
|
|
assert local_stop["operation_id"] == "op-00000000-0000-4000-8000-000000001452"
|
|
assert local_stop["status"] == "succeeded"
|
|
assert completed["acquisition"]["state"] == "completed"
|
|
assert completed["acquisition"]["result"]["device_stop"] == "not-sent"
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None and record.stage == "prepared"
|
|
assert fixture.prepare_calls == [fixture.stop_operation_id]
|
|
assert fixture.control.stop_calls == 1
|
|
assert runtime.stop_calls == 1
|
|
|
|
|
|
def test_capture_only_cleanup_failure_preserves_s0_and_confirmed_start_truth(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(
|
|
service,
|
|
runtime,
|
|
confirm_first_pcl=False,
|
|
)
|
|
service.stop_acquisition(fixture.request)
|
|
fixture.control.state = "failed"
|
|
fixture.control.failure = {
|
|
"reason_code": "application-connection-binding-lost",
|
|
"modeling_command_attempted": False,
|
|
"safe_to_retry": False,
|
|
}
|
|
runtime.stop_error = RuntimeError("synthetic capture-only cleanup timeout")
|
|
|
|
with pytest.raises(RuntimeError, match="synthetic capture-only cleanup timeout"):
|
|
service.stop_acquisition(
|
|
_stop_request(
|
|
acquisition_id=fixture.request.acquisition_id,
|
|
operation_id="op-00000000-0000-4000-8000-000000001457",
|
|
mode="capture-only",
|
|
)
|
|
)
|
|
|
|
start = service._operations.get(fixture.start_operation_id) # noqa: SLF001
|
|
original_stop = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
local_stop = service._operations.get( # noqa: SLF001
|
|
"op-00000000-0000-4000-8000-000000001457"
|
|
)
|
|
assert start.status == "interrupted"
|
|
assert start.stage_code == "physical-start-active-local-retirement-before-point"
|
|
assert start.error is not None and start.error["side_effect_status"] == "succeeded"
|
|
assert original_stop.stage_code == "physical-stop-classified-not-dispatched"
|
|
assert original_stop.error is not None
|
|
assert original_stop.error["side_effect_status"] == "none"
|
|
assert local_stop.status == "failed"
|
|
assert service._acquisition_stop_operation_id == fixture.stop_operation_id # noqa: SLF001
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None and record.stage == "prepared"
|
|
assert service._acquisition_session_lease is not None # noqa: SLF001
|
|
|
|
|
|
@pytest.mark.parametrize("stage", ["dispatching", "observing"])
|
|
def test_capture_only_preserves_postpublish_stop_unknown_before_local_cleanup(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
stage: str,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
dispatch = ApplicationMqttPublishEvidence(
|
|
operation_key="modeling:stop",
|
|
topic=fixture.envelope.topic,
|
|
payload_sha256=fixture.envelope.payload_sha256,
|
|
qos=2,
|
|
retain=False,
|
|
packet_id=None,
|
|
)
|
|
fixture.coordinator.publish_dispatching(dispatch)
|
|
if stage == "observing":
|
|
fixture.ledger.mark_observing(
|
|
fixture.stop_operation_id,
|
|
publish_call_returned=False,
|
|
packet_id=None,
|
|
)
|
|
monkeypatch.setattr(service, "_seal_acquisition_capture_clock", lambda: None)
|
|
|
|
completed = service.stop_acquisition(
|
|
_stop_request(
|
|
acquisition_id=fixture.request.acquisition_id,
|
|
operation_id=(
|
|
"op-00000000-0000-4000-8000-000000001453"
|
|
if stage == "dispatching"
|
|
else "op-00000000-0000-4000-8000-000000001454"
|
|
),
|
|
mode="capture-only",
|
|
)
|
|
)
|
|
|
|
original_stop = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert original_stop.status == "failed"
|
|
assert original_stop.stage_code == "physical-stop-outcome-unknown-local-retirement"
|
|
assert original_stop.error is not None
|
|
assert original_stop.error["side_effect_status"] == "unknown"
|
|
assert original_stop.error["automatic_replay_allowed"] is False
|
|
assert completed["last_operation"]["status"] == "succeeded"
|
|
assert completed["acquisition"]["result"]["device_stop"] == (
|
|
"physical-outcome-unknown"
|
|
)
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None and record.stage == stage
|
|
assert fixture.prepare_calls == [fixture.stop_operation_id]
|
|
assert fixture.control.stop_calls == 1
|
|
assert runtime.stop_calls == 1
|
|
|
|
|
|
@pytest.mark.parametrize("action", ["capture-only", "abort", "force", "reset", "close"])
|
|
def test_resolved_ready_preducer_dominates_destructive_action_and_pending_start(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
action: str,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(
|
|
service,
|
|
runtime,
|
|
confirm_first_pcl=False,
|
|
)
|
|
service.stop_acquisition(fixture.request)
|
|
_resolve_real_stop_ready(fixture)
|
|
monkeypatch.setattr(service, "_seal_acquisition_capture_clock", lambda: None)
|
|
if action == "close":
|
|
monkeypatch.setattr(service.camera_preview, "close", lambda: None)
|
|
monkeypatch.setattr(runtime, "close", runtime.stop)
|
|
before_record = fixture.ledger.snapshot().record
|
|
before_prepare_calls = list(fixture.prepare_calls)
|
|
before_operation_ids = {
|
|
item["operation_id"]
|
|
for item in service._operations.snapshot() # noqa: SLF001
|
|
}
|
|
|
|
if action == "capture-only":
|
|
result = service.stop_acquisition(
|
|
_stop_request(
|
|
acquisition_id=fixture.request.acquisition_id,
|
|
operation_id="op-00000000-0000-4000-8000-000000001455",
|
|
mode="capture-only",
|
|
)
|
|
)
|
|
assert result["acquisition"]["state"] == "completed"
|
|
elif action == "abort":
|
|
result = service.abort_acquisition(
|
|
_abort_request(
|
|
acquisition_id=fixture.request.acquisition_id,
|
|
operation_id="op-00000000-0000-4000-8000-000000001456",
|
|
)
|
|
)
|
|
assert result["acquisition"]["state"] == "completed"
|
|
elif action == "force":
|
|
with service._acquisition_lifecycle_gate: # noqa: SLF001
|
|
assert service._force_finish_active_acquisition_locally( # noqa: SLF001
|
|
reason_code="test-resolved-ready-dominance",
|
|
require_recovery=False,
|
|
)
|
|
elif action == "reset":
|
|
service.select_connection_mode(
|
|
DesiredConnectionModeRequest(
|
|
connection_mode="quick-connect",
|
|
expected_revision=service._desired_connection_mode_revision, # noqa: SLF001
|
|
reset_scenario=True,
|
|
reset_id="resolved-ready-preducer-reset-0001",
|
|
)
|
|
)
|
|
else:
|
|
service.close()
|
|
|
|
start = service._operations.get(fixture.start_operation_id) # noqa: SLF001
|
|
stop = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert start.status == "interrupted"
|
|
assert start.stage_code == "physical-start-active-but-no-point-before-standby"
|
|
assert start.error is not None
|
|
assert start.error["side_effect_status"] == "succeeded"
|
|
assert start.error["automatic_replay_allowed"] is False
|
|
assert stop.status == "succeeded"
|
|
assert stop.stage_code == "device-standby-confirmed"
|
|
assert service._acquisition is not None # noqa: SLF001
|
|
assert service._acquisition.result is not None # noqa: SLF001
|
|
assert service._acquisition.result["device_state"] == "ready" # noqa: SLF001
|
|
assert service._acquisition.result["device_stop"] == "protocol-confirmed" # noqa: SLF001
|
|
assert fixture.ledger.snapshot().record == before_record
|
|
assert fixture.prepare_calls == before_prepare_calls
|
|
assert fixture.control.stop_calls == 1
|
|
assert runtime.stop_calls == 1
|
|
after_operation_ids = {
|
|
item["operation_id"]
|
|
for item in service._operations.snapshot() # noqa: SLF001
|
|
}
|
|
assert after_operation_ids == before_operation_ids
|
|
|
|
|
|
def test_local_force_finish_fences_prepared_stop_then_settles_none_before_cleanup(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
monkeypatch.setattr(service, "_seal_acquisition_capture_clock", lambda: None)
|
|
|
|
with service._acquisition_lifecycle_gate: # noqa: SLF001
|
|
assert service._force_finish_active_acquisition_locally( # noqa: SLF001
|
|
reason_code="test-local-retirement",
|
|
require_recovery=False,
|
|
)
|
|
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.status == "failed"
|
|
assert operation.stage_code == "physical-stop-classified-not-dispatched"
|
|
assert operation.error is not None
|
|
assert operation.error["side_effect_status"] == "none"
|
|
assert operation.error["physical_command_sent"] is False
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None and record.stage == "prepared"
|
|
assert service._acquisition is not None # noqa: SLF001
|
|
assert service._acquisition.state == "interrupted" # noqa: SLF001
|
|
assert service._acquisition.result is not None # noqa: SLF001
|
|
assert service._acquisition.result["device_stop"] == "not-sent" # noqa: SLF001
|
|
assert service._acquisition_stop_operation_id == fixture.stop_operation_id # noqa: SLF001
|
|
assert runtime.stop_calls == 1
|
|
|
|
|
|
def test_local_force_finish_preserves_confirmed_start_truth_before_first_point(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(
|
|
service,
|
|
runtime,
|
|
confirm_first_pcl=False,
|
|
)
|
|
service.stop_acquisition(fixture.request)
|
|
start_before = service._operations.get(fixture.start_operation_id) # noqa: SLF001
|
|
assert start_before.status == "running"
|
|
monkeypatch.setattr(service, "_seal_acquisition_capture_clock", lambda: None)
|
|
|
|
with service._acquisition_lifecycle_gate: # noqa: SLF001
|
|
assert service._force_finish_active_acquisition_locally( # noqa: SLF001
|
|
reason_code="test-local-retirement",
|
|
require_recovery=False,
|
|
)
|
|
|
|
start = service._operations.get(fixture.start_operation_id) # noqa: SLF001
|
|
assert start.status == "interrupted"
|
|
assert start.stage_code == "physical-start-active-local-retirement-before-point"
|
|
assert start.error is not None
|
|
assert start.error["side_effect_status"] == "succeeded"
|
|
assert start.error["automatic_replay_allowed"] is False
|
|
stop = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert stop.stage_code == "physical-stop-classified-not-dispatched"
|
|
assert stop.error is not None and stop.error["side_effect_status"] == "none"
|
|
|
|
|
|
def test_local_force_finish_preserves_dispatching_stop_as_outcome_unknown(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
dispatch = ApplicationMqttPublishEvidence(
|
|
operation_key="modeling:stop",
|
|
topic=fixture.envelope.topic,
|
|
payload_sha256=fixture.envelope.payload_sha256,
|
|
qos=2,
|
|
retain=False,
|
|
packet_id=None,
|
|
)
|
|
assert service._k1_command_dispatch_gate.acquire(blocking=False) # noqa: SLF001
|
|
try:
|
|
fixture.coordinator.publish_dispatching(dispatch)
|
|
finally:
|
|
service._k1_command_dispatch_gate.release() # noqa: SLF001
|
|
monkeypatch.setattr(service, "_seal_acquisition_capture_clock", lambda: None)
|
|
|
|
with service._acquisition_lifecycle_gate: # noqa: SLF001
|
|
assert service._force_finish_active_acquisition_locally( # noqa: SLF001
|
|
reason_code="test-local-retirement",
|
|
require_recovery=False,
|
|
)
|
|
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.status == "failed"
|
|
assert operation.stage_code == "physical-stop-outcome-unknown-local-retirement"
|
|
assert operation.error is not None
|
|
assert operation.error["side_effect_status"] == "unknown"
|
|
assert operation.error["automatic_replay_allowed"] is False
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None and record.stage == "dispatching"
|
|
assert service._acquisition is not None # noqa: SLF001
|
|
assert service._acquisition.state == "interrupted" # noqa: SLF001
|
|
assert service._acquisition.result is not None # noqa: SLF001
|
|
assert service._acquisition.result["device_stop"] == "physical-outcome-unknown" # noqa: SLF001
|
|
assert "physical_command_sent" not in service._acquisition.result # noqa: SLF001
|
|
assert service._acquisition_stop_operation_id == fixture.stop_operation_id # noqa: SLF001
|
|
|
|
|
|
def test_local_force_finish_defers_without_mutation_while_dispatch_gate_is_owned(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
acquisition = service._acquisition # noqa: SLF001
|
|
assert acquisition is not None
|
|
before_revision = acquisition.state_revision
|
|
before_recovery_generation = service._active_stream_recovery_generation # noqa: SLF001
|
|
|
|
assert service._k1_command_dispatch_gate.acquire(blocking=False) # noqa: SLF001
|
|
try:
|
|
with service._acquisition_lifecycle_gate, pytest.raises( # noqa: SLF001
|
|
facade_module.LocalAcquisitionLifecycleError,
|
|
match="publish-переходом STOP",
|
|
) as raised:
|
|
service._force_finish_active_acquisition_locally( # noqa: SLF001
|
|
reason_code="test-local-retirement",
|
|
require_recovery=False,
|
|
)
|
|
finally:
|
|
service._k1_command_dispatch_gate.release() # noqa: SLF001
|
|
|
|
assert raised.value.reason_code == "acquisition-stop-dispatch-retirement-pending"
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.status == "running"
|
|
assert operation.error is None
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None and record.stage == "prepared"
|
|
assert service._acquisition is acquisition # noqa: SLF001
|
|
assert acquisition.state == "awaiting_external_stop"
|
|
assert acquisition.state_revision == before_revision
|
|
assert service._acquisition_stop_operation_id == fixture.stop_operation_id # noqa: SLF001
|
|
assert ( # noqa: SLF001
|
|
service._active_stream_recovery_generation == before_recovery_generation
|
|
)
|
|
assert fixture.control.state == "stop-requested"
|
|
assert runtime.stop_calls == 0
|
|
|
|
|
|
def test_scenario_reset_fences_prepared_stop_before_local_cleanup_and_durable_retirement(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
monkeypatch.setattr(service, "_seal_acquisition_capture_clock", lambda: None)
|
|
|
|
reset = service.select_connection_mode(
|
|
DesiredConnectionModeRequest(
|
|
connection_mode="quick-connect",
|
|
expected_revision=service.state()["desired_connection_mode_revision"],
|
|
reset_scenario=True,
|
|
reset_id="prepared-stop-reset-0001",
|
|
)
|
|
)
|
|
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.status == "failed"
|
|
assert operation.stage_code == "physical-stop-classified-not-dispatched"
|
|
assert operation.error is not None
|
|
assert operation.error["side_effect_status"] == "none"
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None
|
|
assert record.operation_id == fixture.stop_operation_id
|
|
assert record.stage == "resolved"
|
|
assert record.resolution == "not-dispatched"
|
|
assert reset["connection_scenario_reset"]["physical_disposition"] == "not-dispatched"
|
|
assert reset["connection_scenario_reset"]["device_command_performed"] is False
|
|
assert reset["acquisition"]["state"] == "interrupted"
|
|
assert service._acquisition_stop_operation_id is None # noqa: SLF001
|
|
assert runtime.stop_calls == 1
|
|
|
|
|
|
def test_scenario_reset_dispatch_gate_defer_preserves_owner_then_exact_retry_converges(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
monkeypatch.setattr(service, "_seal_acquisition_capture_clock", lambda: None)
|
|
request = DesiredConnectionModeRequest(
|
|
connection_mode="quick-connect",
|
|
expected_revision=service.state()["desired_connection_mode_revision"],
|
|
reset_scenario=True,
|
|
reset_id="prepared-stop-reset-retry-0001",
|
|
)
|
|
|
|
assert service._k1_command_dispatch_gate.acquire(blocking=False) # noqa: SLF001
|
|
try:
|
|
with pytest.raises(
|
|
facade_module.LocalAcquisitionLifecycleError,
|
|
match="publish-переходом STOP",
|
|
) as raised:
|
|
service.select_connection_mode(request)
|
|
finally:
|
|
service._k1_command_dispatch_gate.release() # noqa: SLF001
|
|
|
|
assert raised.value.reason_code == "acquisition-stop-dispatch-retirement-pending"
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.status == "running"
|
|
assert operation.error is None
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None and record.stage == "prepared"
|
|
assert service._acquisition is not None # noqa: SLF001
|
|
assert service._acquisition.state == "awaiting_external_stop" # noqa: SLF001
|
|
assert service._acquisition_stop_operation_id == fixture.stop_operation_id # noqa: SLF001
|
|
assert runtime.stop_calls == 0
|
|
|
|
retried = service.select_connection_mode(request)
|
|
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.stage_code == "physical-stop-classified-not-dispatched"
|
|
assert operation.error is not None
|
|
assert operation.error["side_effect_status"] == "none"
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None and record.resolution == "not-dispatched"
|
|
assert retried["connection_scenario_reset"]["physical_disposition"] == "not-dispatched"
|
|
|
|
|
|
def test_scenario_reset_retires_dispatching_stop_as_outcome_unknown_never_cancelled(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
dispatch = ApplicationMqttPublishEvidence(
|
|
operation_key="modeling:stop",
|
|
topic=fixture.envelope.topic,
|
|
payload_sha256=fixture.envelope.payload_sha256,
|
|
qos=2,
|
|
retain=False,
|
|
packet_id=None,
|
|
)
|
|
assert service._k1_command_dispatch_gate.acquire(blocking=False) # noqa: SLF001
|
|
try:
|
|
fixture.coordinator.publish_dispatching(dispatch)
|
|
finally:
|
|
service._k1_command_dispatch_gate.release() # noqa: SLF001
|
|
monkeypatch.setattr(service, "_seal_acquisition_capture_clock", lambda: None)
|
|
|
|
reset = service.select_connection_mode(
|
|
DesiredConnectionModeRequest(
|
|
connection_mode="quick-connect",
|
|
expected_revision=service.state()["desired_connection_mode_revision"],
|
|
reset_scenario=True,
|
|
reset_id="dispatching-stop-reset-0001",
|
|
)
|
|
)
|
|
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.status == "failed"
|
|
assert operation.stage_code == "physical-stop-outcome-unknown-local-retirement"
|
|
assert operation.error is not None
|
|
assert operation.error["side_effect_status"] == "unknown"
|
|
assert operation.error["automatic_replay_allowed"] is False
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None
|
|
assert record.stage == "resolved"
|
|
assert record.resolution == "operator-retired-outcome-unknown"
|
|
assert reset["connection_scenario_reset"]["physical_disposition"] == (
|
|
"operator-retired-outcome-unknown"
|
|
)
|
|
assert service._acquisition_stop_operation_id is None # noqa: SLF001
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("session_state", "expected_message", "expected_physical_state"),
|
|
[
|
|
(
|
|
"scanning",
|
|
"acquisition.recovery.scanning_adoption_pending",
|
|
"active",
|
|
),
|
|
(
|
|
"ready",
|
|
"acquisition.recovery.device_standby_observed",
|
|
"standby",
|
|
),
|
|
],
|
|
)
|
|
def test_retained_prepared_stop_classification_reserves_local_owner_from_durable_audit(
|
|
tmp_path: Path,
|
|
session_state: str,
|
|
expected_message: str,
|
|
expected_physical_state: str,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
owner = _settle_real_prepared_stop_before_publish(fixture)
|
|
original_acquisition = owner.acquisition
|
|
original_lease = owner.session_lease
|
|
original_out_dir = owner.out_dir
|
|
|
|
reconciled = _classify_real_prepared_stop(
|
|
fixture,
|
|
owner,
|
|
session_state=session_state,
|
|
reconciliation_id=f"test-{session_state}-retained",
|
|
)
|
|
|
|
physical = fixture.coordinator.snapshot()
|
|
assert reconciled["operation_id"] == fixture.stop_operation_id
|
|
assert reconciled["stage"] == "resolved"
|
|
assert reconciled["resolution"] == "not-dispatched"
|
|
assert physical["reconciled_physical_state"] == expected_physical_state
|
|
assert service._acquisition is original_acquisition # noqa: SLF001
|
|
assert service._acquisition_session_lease is original_lease # noqa: SLF001
|
|
assert service._acquisition_out_dir == original_out_dir # noqa: SLF001
|
|
assert service._acquisition.message_code == expected_message # noqa: SLF001
|
|
assert runtime.stop_calls == 0
|
|
assert fixture.control.stop_calls == 1
|
|
stop_operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert stop_operation.stage_code == "physical-stop-classified-not-dispatched"
|
|
assert stop_operation.error is not None
|
|
assert stop_operation.error["side_effect_status"] == "none"
|
|
|
|
|
|
@pytest.mark.parametrize("session_state", ["ready", "scanning"])
|
|
def test_resolved_unclassified_stop_compatibility_classifies_without_new_command(
|
|
tmp_path: Path,
|
|
session_state: str,
|
|
) -> None:
|
|
"""A startup-resolved legacy STOP still takes the read-only classifier."""
|
|
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
_settle_real_prepared_stop_before_publish(fixture)
|
|
fixture.ledger.resolve(
|
|
fixture.stop_operation_id,
|
|
resolution="not-dispatched",
|
|
)
|
|
fixture.coordinator = LedgerPhysicalCommandCoordinator(fixture.ledger)
|
|
service._physical_command_coordinator = fixture.coordinator # noqa: SLF001
|
|
compatibility = fixture.coordinator.snapshot()
|
|
compatibility_record = compatibility["record"]
|
|
assert isinstance(compatibility_record, dict)
|
|
compatibility_revision = compatibility_record["revision"]
|
|
assert compatibility["status"] == "resolved"
|
|
assert compatibility["requires_reconciliation"] is True
|
|
assert compatibility["resolved_unclassified_stop_recovery_required"] is True
|
|
assert compatibility["reconciliation_ready"] is False
|
|
before_prepare_calls = list(fixture.prepare_calls)
|
|
before_control_stop_calls = fixture.control.stop_calls
|
|
|
|
reconciliation_id = f"test-resolved-unclassified-{session_state}"
|
|
scanning = session_state == "scanning"
|
|
runtime_binding = PhysicalCommandRuntimeBinding(
|
|
vendor_device_id_sha256="a" * 64,
|
|
device_serial_sha256="b" * 64,
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
intent_id=fixture.binding.intent_id,
|
|
transport_ref=fixture.binding.transport_ref,
|
|
connection_mode=fixture.binding.connection_mode,
|
|
target_ipv4=fixture.binding.target_ipv4,
|
|
target_port=fixture.binding.target_port,
|
|
host_path_epoch=fixture.binding.host_path_epoch + 1,
|
|
control_session_id=f"fresh-{reconciliation_id}",
|
|
producer_generation=2,
|
|
)
|
|
fixture.coordinator.prepare_read_only_bootstrap()
|
|
fixture.coordinator.application_response(
|
|
ApplicationMqttResponseEvidence(
|
|
operation_key=f"bootstrap:{reconciliation_id}:DeviceInfoRequest",
|
|
response_topic="lixel/application/response/device_info",
|
|
payload_sha256="f" * 64,
|
|
modeling_action=None,
|
|
result_code=None,
|
|
success=None,
|
|
observed_at_utc="2026-08-12T08:10:59.000Z",
|
|
)
|
|
)
|
|
fixture.coordinator.bind_control_session(runtime_binding)
|
|
fixture.coordinator.device_status(
|
|
ApplicationMqttDeviceStatusEvidence(
|
|
vendor_device_id_sha256=runtime_binding.vendor_device_id_sha256,
|
|
device_serial_sha256=runtime_binding.device_serial_sha256,
|
|
session_state=session_state, # type: ignore[arg-type]
|
|
session_state_code=MODELING_STATE_BASE + (302 if scanning else 300),
|
|
project_bound=scanning,
|
|
project_id_sha256="c" * 64 if scanning else None,
|
|
init_ready=scanning,
|
|
status_message_sha256="e" * 64,
|
|
mqtt_retained=False,
|
|
observed_at_utc="2026-08-12T08:11:00.000Z",
|
|
)
|
|
)
|
|
reconciled = fixture.coordinator.reconcile_unresolved(
|
|
reconciliation_id=reconciliation_id,
|
|
)
|
|
|
|
latest = reconciled["reconciliations"][-1]
|
|
original_attempt = latest["original_attempt"]
|
|
assert reconciled["revision"] == compatibility_revision + 1
|
|
assert reconciled["operation_id"] == fixture.stop_operation_id
|
|
assert reconciled["stage"] == "resolved"
|
|
assert reconciled["resolution"] == "not-dispatched"
|
|
assert latest["kind"] == "prepared-stop-classification"
|
|
assert latest["resolution"] == (
|
|
"physical-active-observed"
|
|
if session_state == "scanning"
|
|
else "physical-standby-observed"
|
|
)
|
|
assert original_attempt["stage"] == "resolved"
|
|
assert original_attempt["resolution"] == "not-dispatched"
|
|
assert original_attempt["publish_call_returned"] is None
|
|
assert original_attempt["packet_id"] is None
|
|
assert original_attempt["qos2_completed"] is False
|
|
physical = fixture.coordinator.snapshot()
|
|
matcher = (
|
|
service._matching_classified_prepared_stop_active # noqa: SLF001
|
|
if scanning
|
|
else service._matching_classified_prepared_stop_standby # noqa: SLF001
|
|
)
|
|
assert matcher(
|
|
physical,
|
|
acquisition_id=reconciled["acquisition_id"],
|
|
stop_operation_id=fixture.stop_operation_id,
|
|
start_operation_id=fixture.start_operation_id,
|
|
verified_control=latest["verified_binding"]["connection"],
|
|
)
|
|
assert fixture.prepare_calls == before_prepare_calls
|
|
assert fixture.control.stop_calls == before_control_stop_calls
|
|
assert runtime.stop_calls == 0
|
|
|
|
|
|
def test_retained_prepared_stop_commit_and_local_reservation_are_one_lifecycle_transaction(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
owner = _settle_real_prepared_stop_before_publish(fixture)
|
|
fixture.coordinator.prepare_read_only_bootstrap()
|
|
fixture.coordinator.application_response(
|
|
ApplicationMqttResponseEvidence(
|
|
operation_key="bootstrap:test-atomic:DeviceInfoRequest",
|
|
response_topic="lixel/application/response/device_info",
|
|
payload_sha256="f" * 64,
|
|
modeling_action=None,
|
|
result_code=None,
|
|
success=None,
|
|
observed_at_utc="2026-08-12T08:01:59.000Z",
|
|
)
|
|
)
|
|
binding = PhysicalCommandRuntimeBinding(
|
|
vendor_device_id_sha256="a" * 64,
|
|
device_serial_sha256="b" * 64,
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
intent_id=fixture.binding.intent_id,
|
|
transport_ref=fixture.binding.transport_ref,
|
|
connection_mode=fixture.binding.connection_mode,
|
|
target_ipv4=fixture.binding.target_ipv4,
|
|
target_port=fixture.binding.target_port,
|
|
host_path_epoch=fixture.binding.host_path_epoch + 1,
|
|
control_session_id="fresh-atomic-classification",
|
|
producer_generation=2,
|
|
)
|
|
fixture.coordinator.bind_control_session(binding)
|
|
fixture.coordinator.device_status(
|
|
ApplicationMqttDeviceStatusEvidence(
|
|
vendor_device_id_sha256="a" * 64,
|
|
device_serial_sha256="b" * 64,
|
|
session_state="scanning",
|
|
session_state_code=MODELING_STATE_BASE + 302,
|
|
project_bound=True,
|
|
project_id_sha256="c" * 64,
|
|
init_ready=True,
|
|
status_message_sha256="e" * 64,
|
|
mqtt_retained=False,
|
|
observed_at_utc="2026-08-12T08:02:00.000Z",
|
|
)
|
|
)
|
|
committed = threading.Event()
|
|
allow_reservation = threading.Event()
|
|
state_finished = threading.Event()
|
|
errors: list[BaseException] = []
|
|
|
|
def reconcile_after_pause() -> dict[str, object]:
|
|
record = fixture.coordinator.reconcile_unresolved(
|
|
reconciliation_id="test-atomic-classification",
|
|
)
|
|
committed.set()
|
|
assert allow_reservation.wait(timeout=5)
|
|
return record
|
|
|
|
def classify() -> None:
|
|
try:
|
|
service._classify_retained_prepared_stop_owned( # noqa: SLF001
|
|
owner=owner,
|
|
reconciliation_id="test-atomic-classification",
|
|
reconcile=reconcile_after_pause,
|
|
)
|
|
except BaseException as exc: # pragma: no cover - assertion surface
|
|
errors.append(exc)
|
|
|
|
def poll_state() -> None:
|
|
try:
|
|
service.state()
|
|
except BaseException as exc: # pragma: no cover - assertion surface
|
|
errors.append(exc)
|
|
finally:
|
|
state_finished.set()
|
|
|
|
classify_thread = threading.Thread(target=classify)
|
|
classify_thread.start()
|
|
assert committed.wait(timeout=5)
|
|
poll_thread = threading.Thread(target=poll_state)
|
|
poll_thread.start()
|
|
assert state_finished.wait(timeout=0.05) is False
|
|
allow_reservation.set()
|
|
classify_thread.join(timeout=5)
|
|
poll_thread.join(timeout=5)
|
|
|
|
assert not errors
|
|
assert state_finished.is_set()
|
|
assert service._acquisition is owner.acquisition # noqa: SLF001
|
|
assert service._acquisition_session_lease is owner.session_lease # noqa: SLF001
|
|
assert service._acquisition.message_code == ( # noqa: SLF001
|
|
"acquisition.recovery.scanning_adoption_pending"
|
|
)
|
|
|
|
|
|
def test_exact_old_stop_retry_is_idempotent_during_scanning_adoption_pending(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
owner = _settle_real_prepared_stop_before_publish(fixture)
|
|
_classify_real_prepared_stop(
|
|
fixture,
|
|
owner,
|
|
session_state="scanning",
|
|
reconciliation_id="test-old-s0-retry-pending",
|
|
)
|
|
before_record = fixture.ledger.snapshot().record
|
|
before_operations = tuple(service._operations.snapshot()) # noqa: SLF001
|
|
before_control_calls = fixture.control.stop_calls
|
|
before_prepare_calls = list(fixture.prepare_calls)
|
|
|
|
repeated = service.stop_acquisition(fixture.request)
|
|
|
|
assert repeated["acquisition"]["message_code"] == (
|
|
"acquisition.recovery.scanning_adoption_pending"
|
|
)
|
|
assert fixture.ledger.snapshot().record == before_record
|
|
assert tuple(service._operations.snapshot()) == before_operations # noqa: SLF001
|
|
assert fixture.control.stop_calls == before_control_calls
|
|
assert fixture.prepare_calls == before_prepare_calls
|
|
assert service._prepared_stop_recovery_owner is owner # noqa: SLF001
|
|
assert runtime.stop_calls == 0
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"action",
|
|
["abort", "capture-only", "force", "reset", "close"],
|
|
)
|
|
def test_ready_local_projection_pending_rejects_destructive_action_without_mutation(
|
|
tmp_path: Path,
|
|
action: str,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
owner = _settle_real_prepared_stop_before_publish(fixture)
|
|
_classify_real_prepared_stop(
|
|
fixture,
|
|
owner,
|
|
session_state="ready",
|
|
reconciliation_id=f"test-ready-pending-{action}",
|
|
)
|
|
acquisition = service._acquisition # noqa: SLF001
|
|
assert acquisition is owner.acquisition
|
|
before_record = fixture.ledger.snapshot().record
|
|
before_operations = tuple(service._operations.snapshot()) # noqa: SLF001
|
|
before_state = (
|
|
acquisition.state,
|
|
acquisition.state_revision,
|
|
acquisition.message_code,
|
|
acquisition.result,
|
|
)
|
|
before_stop_pointer = service._acquisition_stop_operation_id # noqa: SLF001
|
|
before_start_pointer = service._acquisition_start_operation_id # noqa: SLF001
|
|
before_control_calls = fixture.control.stop_calls
|
|
before_prepare_calls = list(fixture.prepare_calls)
|
|
|
|
with pytest.raises(facade_module.LocalAcquisitionLifecycleError) as raised:
|
|
if action == "abort":
|
|
service.abort_acquisition(
|
|
_abort_request(acquisition_id=acquisition.acquisition_id)
|
|
)
|
|
elif action == "capture-only":
|
|
service.stop_acquisition(
|
|
_stop_request(
|
|
acquisition_id=acquisition.acquisition_id,
|
|
operation_id=f"op-ready-pending-capture-{action}",
|
|
mode="capture-only",
|
|
)
|
|
)
|
|
elif action == "force":
|
|
service.force_finish_acquisition_locally(
|
|
_force_finish_request(
|
|
acquisition_id=acquisition.acquisition_id,
|
|
expected_state_revision=acquisition.state_revision,
|
|
expected_recovery_generation=(
|
|
service._active_stream_recovery_generation # noqa: SLF001
|
|
),
|
|
)
|
|
)
|
|
elif action == "reset":
|
|
service.select_connection_mode(
|
|
DesiredConnectionModeRequest(
|
|
connection_mode="quick-connect",
|
|
expected_revision=service._desired_connection_mode_revision, # noqa: SLF001
|
|
reset_scenario=True,
|
|
reset_id="ready-pending-reset-0001",
|
|
)
|
|
)
|
|
else:
|
|
service.close()
|
|
|
|
assert raised.value.reason_code == "acquisition-prepared-stop-adoption-pending"
|
|
assert fixture.ledger.snapshot().record == before_record
|
|
assert tuple(service._operations.snapshot()) == before_operations # noqa: SLF001
|
|
assert (
|
|
acquisition.state,
|
|
acquisition.state_revision,
|
|
acquisition.message_code,
|
|
acquisition.result,
|
|
) == before_state
|
|
assert service._acquisition_stop_operation_id == before_stop_pointer # noqa: SLF001
|
|
assert service._acquisition_start_operation_id == before_start_pointer # noqa: SLF001
|
|
assert service._prepared_stop_recovery_owner is owner # noqa: SLF001
|
|
assert fixture.control.stop_calls == before_control_calls
|
|
assert fixture.prepare_calls == before_prepare_calls
|
|
assert runtime.stop_calls == 0
|
|
assert service._connection_scenario_reset_pending is None # noqa: SLF001
|
|
assert service._service_close_requested.is_set() is False # noqa: SLF001
|
|
|
|
|
|
def test_classified_scanning_adopts_same_receiver_and_allows_one_fresh_stop(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
owner = _settle_real_prepared_stop_before_publish(fixture)
|
|
reconciled = _classify_real_prepared_stop(
|
|
fixture,
|
|
owner,
|
|
session_state="scanning",
|
|
reconciliation_id="test-scanning-in-place",
|
|
)
|
|
latest = reconciled["reconciliations"][-1]
|
|
verified_binding = latest["verified_binding"]["connection"]
|
|
fixture.control.state = "scanning"
|
|
fixture.control.state_revision += 1
|
|
fixture.control.failure = None
|
|
fixture.control.verified_control = {
|
|
**dict(verified_binding),
|
|
"logical_device_id": "known-k1",
|
|
"compatibility_profile_id": XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
}
|
|
old_device_session_id = owner.acquisition.device_session_id
|
|
service._device_session_id = "fresh-in-place-device-session" # noqa: SLF001
|
|
runtime_start_calls = list(runtime.start_calls)
|
|
|
|
service._adopt_classified_prepared_stop_scanning_in_place( # noqa: SLF001
|
|
owner=owner,
|
|
reconciliation_id="test-scanning-in-place",
|
|
reconciled_record=reconciled,
|
|
)
|
|
|
|
assert service._acquisition is owner.acquisition # noqa: SLF001
|
|
assert service._acquisition_session_lease is owner.session_lease # noqa: SLF001
|
|
assert service._acquisition_out_dir == owner.out_dir # noqa: SLF001
|
|
assert service._acquisition.device_session_id != old_device_session_id # noqa: SLF001
|
|
assert service._acquisition.device_session_id == ( # noqa: SLF001
|
|
"fresh-in-place-device-session"
|
|
)
|
|
assert service._acquisition.state == "acquiring" # noqa: SLF001
|
|
assert service._acquisition_stop_operation_id is None # noqa: SLF001
|
|
assert runtime.start_calls == runtime_start_calls
|
|
|
|
# Hold the durable gap closure exactly where its fsync may block. A public
|
|
# STOP must publish priority without waiting for lifecycle ownership; once
|
|
# the store resumes, that priority prevents this already-published PCL from
|
|
# granting camera authority and the fresh STOP owns the next durable edge.
|
|
store = service._active_acquisition_checkpoint # noqa: SLF001
|
|
assert store is not None
|
|
original_rebind_active = store.rebind_active
|
|
rebind_entered = threading.Event()
|
|
release_rebind = threading.Event()
|
|
|
|
def blocked_rebind_active(**kwargs: Any) -> Any:
|
|
rebind_entered.set()
|
|
assert release_rebind.wait(timeout=2.0)
|
|
return original_rebind_active(**kwargs)
|
|
|
|
monkeypatch.setattr(store, "rebind_active", blocked_rebind_active)
|
|
camera_calls: list[str] = []
|
|
|
|
def forbidden_camera_start(*_: object, **__: object) -> dict[str, object]:
|
|
camera_calls.append("camera-start")
|
|
return service.camera_preview.snapshot()
|
|
|
|
monkeypatch.setattr(
|
|
service.camera_preview,
|
|
"activate_recording_producer",
|
|
forbidden_camera_start,
|
|
)
|
|
monkeypatch.setattr(
|
|
service.camera_preview,
|
|
"retry_recording_producer",
|
|
forbidden_camera_start,
|
|
)
|
|
frame = DecodedPointCloudView(
|
|
context=ConsumerFrameContext(
|
|
sequence=1,
|
|
captured_at_epoch_ns=1,
|
|
received_monotonic_ns=1,
|
|
processing_started_monotonic_ns=1,
|
|
encoded_size_bytes=16,
|
|
live=True,
|
|
),
|
|
frame_id="map",
|
|
positions_xyz=((0.0, 0.0, 0.0),),
|
|
)
|
|
runtime.phase = "live"
|
|
runtime.source_mode = "live"
|
|
runtime.source_ready = True
|
|
runtime.pcl_frames = 1
|
|
publish_errors: list[BaseException] = []
|
|
|
|
def publish_first_pcl() -> None:
|
|
try:
|
|
service._observe_published_runtime_envelope( # noqa: SLF001
|
|
frame,
|
|
runtime.producer_generation,
|
|
)
|
|
except BaseException as exc: # pragma: no cover - asserted below
|
|
publish_errors.append(exc)
|
|
|
|
fresh_stop_id = "op-00000000-0000-4000-8000-000000001402"
|
|
fresh_request = _stop_request(
|
|
acquisition_id=owner.lineage.acquisition_id,
|
|
operation_id=fresh_stop_id,
|
|
idempotency_key="classified-scanning-fresh-stop",
|
|
mode="graceful",
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
expected_control_session_generation=fixture.control.session_generation,
|
|
expected_control_state_revision=fixture.control.state_revision,
|
|
)
|
|
stop_results: list[dict[str, Any]] = []
|
|
stop_errors: list[BaseException] = []
|
|
|
|
def request_fresh_stop() -> None:
|
|
try:
|
|
stop_results.append(service.stop_acquisition(fresh_request))
|
|
except BaseException as exc: # pragma: no cover - asserted below
|
|
stop_errors.append(exc)
|
|
|
|
publish_thread = threading.Thread(target=publish_first_pcl)
|
|
stop_thread = threading.Thread(target=request_fresh_stop)
|
|
publish_thread.start()
|
|
try:
|
|
assert rebind_entered.wait(timeout=2.0)
|
|
stop_thread.start()
|
|
deadline = time.monotonic() + 2.0
|
|
while time.monotonic() < deadline:
|
|
with service._lock: # noqa: SLF001
|
|
if service._camera_stop_priority_counts.get( # noqa: SLF001
|
|
owner.lineage.acquisition_id,
|
|
0,
|
|
):
|
|
break
|
|
time.sleep(0.005)
|
|
with service._lock: # noqa: SLF001
|
|
assert service._camera_stop_priority_counts.get( # noqa: SLF001
|
|
owner.lineage.acquisition_id,
|
|
0,
|
|
) == 1
|
|
assert stop_thread.is_alive()
|
|
finally:
|
|
release_rebind.set()
|
|
publish_thread.join(timeout=2.0)
|
|
if stop_thread.ident is not None:
|
|
stop_thread.join(timeout=2.0)
|
|
|
|
assert publish_thread.is_alive() is False
|
|
assert stop_thread.is_alive() is False
|
|
assert publish_errors == []
|
|
assert stop_errors == []
|
|
assert len(stop_results) == 1
|
|
fresh = stop_results[0]
|
|
assert camera_calls == []
|
|
with service._lock: # noqa: SLF001
|
|
assert service._classified_stop_rebind_pending is None # noqa: SLF001
|
|
assert service._classified_stop_rebind_inflight is None # noqa: SLF001
|
|
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None
|
|
assert record.operation_id == fresh_stop_id
|
|
assert record.parent_operation_id == fixture.stop_operation_id
|
|
assert record.stage == "prepared"
|
|
assert fresh["acquisition"]["state"] == "awaiting_external_stop"
|
|
assert fixture.prepare_calls == [fixture.stop_operation_id, fresh_stop_id]
|
|
assert runtime.start_calls == runtime_start_calls
|
|
|
|
|
|
def test_blocked_checkpoint_fsync_starts_stop_deadline_and_expires_before_prepare(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
owner = _settle_real_prepared_stop_before_publish(fixture)
|
|
reconciled = _classify_real_prepared_stop(
|
|
fixture,
|
|
owner,
|
|
session_state="scanning",
|
|
reconciliation_id="test-scanning-stop-deadline-fsync",
|
|
)
|
|
verified_binding = reconciled["reconciliations"][-1]["verified_binding"]["connection"]
|
|
fixture.control.state = "scanning"
|
|
fixture.control.state_revision += 1
|
|
fixture.control.failure = None
|
|
fixture.control.verified_control = {
|
|
**dict(verified_binding),
|
|
"logical_device_id": "known-k1",
|
|
"compatibility_profile_id": XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
}
|
|
service._device_session_id = "fresh-stop-deadline-session" # noqa: SLF001
|
|
service._adopt_classified_prepared_stop_scanning_in_place( # noqa: SLF001
|
|
owner=owner,
|
|
reconciliation_id="test-scanning-stop-deadline-fsync",
|
|
reconciled_record=reconciled,
|
|
)
|
|
|
|
store = service._active_acquisition_checkpoint # noqa: SLF001
|
|
assert store is not None
|
|
original_rebind_active = store.rebind_active
|
|
rebind_entered = threading.Event()
|
|
release_rebind = threading.Event()
|
|
|
|
def blocked_rebind_active(**kwargs: Any) -> Any:
|
|
rebind_entered.set()
|
|
assert release_rebind.wait(timeout=3.0)
|
|
return original_rebind_active(**kwargs)
|
|
|
|
monkeypatch.setattr(store, "rebind_active", blocked_rebind_active)
|
|
frame = DecodedPointCloudView(
|
|
context=ConsumerFrameContext(
|
|
sequence=1,
|
|
captured_at_epoch_ns=1,
|
|
received_monotonic_ns=1,
|
|
processing_started_monotonic_ns=1,
|
|
encoded_size_bytes=16,
|
|
live=True,
|
|
),
|
|
frame_id="map",
|
|
positions_xyz=((0.0, 0.0, 0.0),),
|
|
)
|
|
runtime.phase = "live"
|
|
runtime.source_mode = "live"
|
|
runtime.source_ready = True
|
|
runtime.pcl_frames = 1
|
|
publish_thread = threading.Thread(
|
|
target=service._observe_published_runtime_envelope, # noqa: SLF001
|
|
kwargs={"envelope": frame, "producer_generation": runtime.producer_generation},
|
|
daemon=True,
|
|
)
|
|
publish_thread.start()
|
|
assert rebind_entered.wait(timeout=2.0)
|
|
|
|
stop_id = "op-00000000-0000-4000-8000-000000001403"
|
|
request = _stop_request(
|
|
acquisition_id=owner.lineage.acquisition_id,
|
|
operation_id=stop_id,
|
|
idempotency_key="classified-scanning-fsync-expired-stop",
|
|
mode="graceful",
|
|
deadline_seconds=1.0,
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
expected_control_session_generation=fixture.control.session_generation,
|
|
expected_control_state_revision=fixture.control.state_revision,
|
|
)
|
|
stop_errors: list[BaseException] = []
|
|
def request_stop() -> None:
|
|
try:
|
|
service.stop_acquisition(request)
|
|
except BaseException as exc: # pragma: no cover - asserted below
|
|
stop_errors.append(exc)
|
|
|
|
stop_thread = threading.Thread(target=request_stop, daemon=True)
|
|
stop_thread.start()
|
|
try:
|
|
priority_deadline = time.monotonic() + 0.5
|
|
while time.monotonic() < priority_deadline:
|
|
operation = next(
|
|
(
|
|
item
|
|
for item in service._operations.snapshot() # noqa: SLF001
|
|
if item["operation_id"] == stop_id
|
|
),
|
|
None,
|
|
)
|
|
with service._lock: # noqa: SLF001
|
|
priority = service._camera_stop_priority_counts.get( # noqa: SLF001
|
|
owner.lineage.acquisition_id,
|
|
0,
|
|
)
|
|
if operation is not None and priority == 1:
|
|
break
|
|
time.sleep(0.005)
|
|
assert operation is not None
|
|
assert operation["status"] == "accepted"
|
|
assert priority == 1
|
|
assert fixture.prepare_calls == [fixture.stop_operation_id]
|
|
assert stop_thread.is_alive()
|
|
deadline = time.monotonic() + 2.0
|
|
while time.monotonic() < deadline and not service._operations.deadline_reached( # noqa: SLF001
|
|
stop_id
|
|
):
|
|
time.sleep(0.01)
|
|
assert service._operations.deadline_reached(stop_id) is True # noqa: SLF001
|
|
finally:
|
|
release_rebind.set()
|
|
publish_thread.join(timeout=3.0)
|
|
stop_thread.join(timeout=3.0)
|
|
|
|
assert publish_thread.is_alive() is False
|
|
assert stop_thread.is_alive() is False
|
|
assert len(stop_errors) == 1
|
|
assert getattr(stop_errors[0], "reason_code", None) == (
|
|
"physical-command-dispatch-deadline-expired"
|
|
)
|
|
operation = service._operations.get(stop_id) # noqa: SLF001
|
|
assert operation.status == "failed"
|
|
assert operation.stage_code == (
|
|
"physical-stop-dispatch-deadline-expired-before-prepare"
|
|
)
|
|
assert operation.error is not None
|
|
assert operation.error["code"] == "physical-command-dispatch-deadline-expired"
|
|
assert operation.error["side_effect_status"] == "none"
|
|
assert operation.error["physical_command_sent"] is False
|
|
assert operation.error["automatic_replay_allowed"] is False
|
|
assert fixture.prepare_calls == [fixture.stop_operation_id]
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None
|
|
assert record.operation_id == fixture.stop_operation_id
|
|
assert record.action == "stop"
|
|
assert record.stage == "resolved"
|
|
assert fixture.control.stop_calls == 1
|
|
|
|
|
|
def test_stop_deadline_after_durable_prepare_retains_exact_no_dispatch_owner(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
original_request_stop = fixture.control.request_stop
|
|
|
|
def expire_after_durable_prepare(**kwargs: object) -> dict[str, object]:
|
|
command_context = kwargs["command_context"]
|
|
fixture.coordinator.prepare(
|
|
command_context,
|
|
action="stop",
|
|
envelope=fixture.envelope,
|
|
)
|
|
fixture.prepare_calls.append(str(command_context.operation_id))
|
|
fixture.coordinator.resolve_prepared_not_dispatched("stop")
|
|
raise ApplicationMqttTransportError(
|
|
"synthetic deadline after STOP PREPARE fsync",
|
|
reason_code="physical-command-dispatch-deadline-expired",
|
|
)
|
|
|
|
fixture.control.request_stop = expire_after_durable_prepare
|
|
try:
|
|
with pytest.raises(
|
|
ApplicationMqttTransportError,
|
|
match="synthetic deadline after STOP PREPARE fsync",
|
|
):
|
|
service.stop_acquisition(fixture.request)
|
|
finally:
|
|
fixture.control.request_stop = original_request_stop
|
|
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None
|
|
assert record.operation_id == fixture.stop_operation_id
|
|
assert record.action == "stop"
|
|
assert record.stage == "resolved"
|
|
assert record.resolution == "not-dispatched"
|
|
assert record.publish_call_returned is None
|
|
assert record.packet_id is None
|
|
assert record.qos2_completed is False
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.status == "failed"
|
|
assert operation.stage_code == "physical-stop-classified-not-dispatched"
|
|
assert operation.error is not None
|
|
assert operation.error["side_effect_status"] == "none"
|
|
assert operation.error["physical_command_sent"] is False
|
|
assert operation.error["automatic_replay_allowed"] is False
|
|
assert fixture.prepare_calls == [fixture.stop_operation_id]
|
|
with service._lock: # noqa: SLF001
|
|
acquisition = service._acquisition # noqa: SLF001
|
|
owner = service._prepared_stop_recovery_owner # noqa: SLF001
|
|
assert service._acquisition_stop_operation_id == fixture.stop_operation_id # noqa: SLF001
|
|
assert acquisition is not None
|
|
assert acquisition.state == "awaiting_external_stop"
|
|
assert owner is not None
|
|
assert owner.acquisition is acquisition
|
|
assert owner.lineage.stop_operation_id == fixture.stop_operation_id
|
|
assert service.camera_preview.snapshot()["recording"]["source_end_expected"] is False
|
|
|
|
# Exact retries observe the terminal journal identity and never prepare or
|
|
# publish another physical STOP, even after the original control socket is
|
|
# no longer admissible for a new command.
|
|
fixture.control.state = "failed"
|
|
fixture.control.state_revision += 1
|
|
fixture.control.failure = {
|
|
"reason_code": "mqtt_network_loop_failed",
|
|
"modeling_command_attempted": False,
|
|
"safe_to_retry": False,
|
|
}
|
|
fixture.control.verified_control = None
|
|
retried = service.stop_acquisition(fixture.request)
|
|
assert retried["contract_version"] == "missioncore.device-plugin-state/v1alpha2"
|
|
assert retried["selected_device_id"] == fixture.binding.transport_ref
|
|
assert retried["acquisition"]["acquisition_id"] == fixture.request.acquisition_id
|
|
assert retried["acquisition"]["state"] in {
|
|
"awaiting_external_stop",
|
|
"interrupted",
|
|
}
|
|
assert retried["last_operation"]["operation_id"] == fixture.stop_operation_id
|
|
assert retried["last_operation"]["status"] == "failed"
|
|
assert fixture.prepare_calls == [fixture.stop_operation_id]
|
|
|
|
with pytest.raises(ValueError, match="different request"):
|
|
service.stop_acquisition(
|
|
fixture.request.model_copy(
|
|
update={
|
|
"idempotency_key": fixture.request.idempotency_key,
|
|
"expected_control_state_revision": (
|
|
fixture.request.expected_control_state_revision + 1
|
|
),
|
|
}
|
|
)
|
|
)
|
|
assert fixture.prepare_calls == [fixture.stop_operation_id]
|
|
|
|
|
|
def test_corrupt_physical_snapshot_never_terminalizes_preadmitted_stop_as_none(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
original_snapshot = fixture.ledger.snapshot
|
|
original_owned = service._stop_acquisition_owned # noqa: SLF001
|
|
entered_owned = threading.Event()
|
|
|
|
def reject_before_prepare(
|
|
request: StopAcquisitionRequest,
|
|
preadmitted_stop: object | None = None,
|
|
) -> dict[str, Any]:
|
|
assert preadmitted_stop is not None
|
|
entered_owned.set()
|
|
raise facade_module.LocalAcquisitionLifecycleError(
|
|
"synthetic admission rejection before physical PREPARE",
|
|
reason_code="synthetic-before-prepare",
|
|
)
|
|
|
|
monkeypatch.setattr(service, "_stop_acquisition_owned", reject_before_prepare)
|
|
monkeypatch.setattr(
|
|
fixture.ledger,
|
|
"snapshot",
|
|
lambda: PhysicalCommandLedgerSnapshot(
|
|
status="corrupt",
|
|
record=None,
|
|
reason_code="physical-command-ledger-corrupt",
|
|
),
|
|
)
|
|
fresh_request = fixture.request.model_copy(
|
|
update={
|
|
"operation_id": "op-00000000-0000-4000-8000-000000001404",
|
|
"idempotency_key": "corrupt-ledger-preadmitted-stop",
|
|
}
|
|
)
|
|
try:
|
|
with pytest.raises(
|
|
facade_module.LocalAcquisitionLifecycleError,
|
|
match="synthetic admission rejection",
|
|
):
|
|
service.stop_acquisition(fresh_request)
|
|
finally:
|
|
monkeypatch.setattr(fixture.ledger, "snapshot", original_snapshot)
|
|
monkeypatch.setattr(service, "_stop_acquisition_owned", original_owned)
|
|
|
|
assert entered_owned.is_set()
|
|
operation = service._operations.get(fresh_request.operation_id) # noqa: SLF001
|
|
assert operation.status == "accepted"
|
|
assert operation.error is None
|
|
assert fixture.prepare_calls == []
|
|
assert original_snapshot().record is not None
|
|
assert original_snapshot().record.operation_id == fixture.start_operation_id # type: ignore[union-attr]
|
|
|
|
|
|
def test_classified_ready_finishes_same_receiver_and_pins_fresh_start_binding(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
owner = _settle_real_prepared_stop_before_publish(fixture)
|
|
_classify_real_prepared_stop(
|
|
fixture,
|
|
owner,
|
|
session_state="ready",
|
|
reconciliation_id="test-ready-in-place",
|
|
)
|
|
monkeypatch.setattr(service, "_seal_acquisition_capture_clock", lambda: None)
|
|
|
|
finished = service.state()
|
|
|
|
stop_operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert finished["acquisition"]["state"] == "interrupted"
|
|
assert finished["acquisition"]["result"]["read_only_recovery"] is True
|
|
assert finished["acquisition"]["result"]["device_stop"] == "not-sent"
|
|
assert finished["acquisition"]["cleanup_pending"] is False
|
|
assert stop_operation.stage_code == "physical-stop-classified-not-dispatched"
|
|
assert stop_operation.error is not None
|
|
assert stop_operation.error["side_effect_status"] == "none"
|
|
assert service._acquisition_session_lease is None # noqa: SLF001
|
|
assert service._prepared_stop_recovery_owner is None # noqa: SLF001
|
|
assert runtime.stop_calls == 1
|
|
|
|
physical = fixture.coordinator.snapshot()
|
|
assert facade_module._physical_command_prepared_stop_ready_successor_pending( # noqa: SLF001
|
|
physical
|
|
)
|
|
with pytest.raises(
|
|
facade_module.NetworkProvisioningConflict,
|
|
) as mutation:
|
|
service._require_physical_command_network_mutation_allowed() # noqa: SLF001
|
|
assert mutation.value.reason_code == "physical-command-reconciliation-required"
|
|
policy = facade_module._connection_policy_projection( # noqa: SLF001
|
|
supervisor=service._connection_supervisor.snapshot(), # noqa: SLF001
|
|
ledger=service._network_mutation_ledger.snapshot(), # noqa: SLF001
|
|
fresh_devices=[],
|
|
provisioning_active=False,
|
|
acquisition_active=False,
|
|
acquisition_cleanup_pending=False,
|
|
acquisition_state="interrupted",
|
|
runtime_active=False,
|
|
application_control_session=fixture.control.snapshot(),
|
|
physical_command=physical,
|
|
ble_runtime={
|
|
"poisoned": False,
|
|
"cleanup_pending": False,
|
|
"active_operation_kind": None,
|
|
},
|
|
lifecycle_process_lease_holders=(),
|
|
network_provisioning_idempotency={},
|
|
network_provisioning_idempotency_available=True,
|
|
semantic_topology_store={"status": "available"},
|
|
device_identity_pin_store={"status": "available"},
|
|
current_device_recovery=None,
|
|
desired_connection_mode=service._desired_connection_mode, # noqa: SLF001
|
|
active_connection_mode=service._connection_mode, # noqa: SLF001
|
|
)
|
|
assert "physical-command-reconciliation-required" not in policy["actions"][
|
|
"start-acquisition"
|
|
]["reason_codes"]
|
|
|
|
# The classified READY binding remains pinned during an ordinary
|
|
# continuation, but an explicit clean-scenario reset must abandon that
|
|
# future START ownership. After the separately requested fresh Scan, the
|
|
# exact UUID is an ordinary one-click provisioning target again.
|
|
reset = service.select_connection_mode(
|
|
DesiredConnectionModeRequest(
|
|
connection_mode="bridge",
|
|
expected_revision=finished["desired_connection_mode_revision"],
|
|
reset_scenario=True,
|
|
reset_id="classified-ready-clean-network-reset-0001",
|
|
)
|
|
)
|
|
assert reset["connection_scenario_reset"]["physical_disposition"] == (
|
|
"operator-retired-outcome-unknown"
|
|
)
|
|
reset_record = fixture.ledger.snapshot().record
|
|
assert reset_record is not None
|
|
assert reset_record.resolution == "operator-retired-outcome-unknown"
|
|
assert reset_record.original_command_outcome == "not-dispatched"
|
|
assert fixture.prepare_calls == [fixture.stop_operation_id]
|
|
|
|
with service._lock: # noqa: SLF001
|
|
service._ble_discovery_generation += 1 # noqa: SLF001
|
|
generation = service._ble_discovery_generation # noqa: SLF001
|
|
service._scenario_reset_successful_scan_generation = generation # noqa: SLF001
|
|
assert service._connection_scenario_reset is not None # noqa: SLF001
|
|
service._scenario_reset_successful_scan_reset_id = ( # noqa: SLF001
|
|
service._connection_scenario_reset["reset_id"]
|
|
)
|
|
service._connection_scenario_reset["active"] = False # noqa: SLF001
|
|
service._connection_scenario_reset[ # noqa: SLF001
|
|
"settled_by_discovery_generation"
|
|
] = generation
|
|
_set_scanned_k1(service, device_id=fixture.binding.transport_ref)
|
|
|
|
scanned = service.state()
|
|
assert scanned["connection_policy"]["facts"][
|
|
"eligible_fresh_transport_refs"
|
|
] == [fixture.binding.transport_ref]
|
|
assert scanned["connection_policy"]["actions"]["provision-fresh-device"][
|
|
"allowed"
|
|
] is True
|
|
assert scanned["connection_policy"]["actions"]["start-acquisition"][
|
|
"allowed"
|
|
] is False
|
|
|
|
writes = _install_successful_fast_ack_bridge_write(monkeypatch)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_schedule_control_bootstrap_continuation",
|
|
lambda **_kwargs: None,
|
|
)
|
|
applied = asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id=fixture.binding.transport_ref,
|
|
ssid="classified-ready-clean-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
operation_id="op-00000000-0000-4000-8000-000000001405",
|
|
expected_mode_revision=scanned[
|
|
"desired_connection_mode_revision"
|
|
],
|
|
expected_discovery_generation=generation,
|
|
expected_reconfiguration_revision=scanned[
|
|
"connection_reconfiguration"
|
|
]["revision"],
|
|
)
|
|
)
|
|
)
|
|
assert writes == ["network-write"]
|
|
assert applied["connection_attempt"]["phase"] == "network_applied"
|
|
reopened = fixture.ledger.snapshot().record
|
|
assert reopened is not None
|
|
assert reopened.original_command_outcome == "not-dispatched"
|
|
assert reopened.reopened_physical_state_requires_reconciliation is True
|
|
assert len(reopened.operator_reconciliation_reopens) == 1
|
|
assert reopened.operator_reconciliation_reopens[-1].reason == (
|
|
"reset-network-intent-read-only-settlement"
|
|
)
|
|
assert fixture.prepare_calls == [fixture.stop_operation_id]
|
|
|
|
|
|
def test_classified_ready_before_first_pcl_interrupts_pending_start_without_new_edge(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(
|
|
service,
|
|
runtime,
|
|
confirm_first_pcl=False,
|
|
)
|
|
start_before = service._operations.get(fixture.start_operation_id) # noqa: SLF001
|
|
assert start_before.status == "running"
|
|
assert service._acquisition_start_operation_id == fixture.start_operation_id # noqa: SLF001
|
|
|
|
service.stop_acquisition(fixture.request)
|
|
owner = _settle_real_prepared_stop_before_publish(fixture)
|
|
_classify_real_prepared_stop(
|
|
fixture,
|
|
owner,
|
|
session_state="ready",
|
|
reconciliation_id="test-ready-before-first-pcl",
|
|
)
|
|
monkeypatch.setattr(service, "_seal_acquisition_capture_clock", lambda: None)
|
|
|
|
finished = service.state()
|
|
|
|
start = service._operations.get(fixture.start_operation_id) # noqa: SLF001
|
|
stop = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert finished["acquisition"]["state"] == "interrupted"
|
|
assert finished["acquisition"]["result"]["device_stop"] == "not-sent"
|
|
assert start.status == "interrupted"
|
|
assert start.stage_code == "physical-start-active-but-no-point-before-standby"
|
|
assert start.error is not None
|
|
assert start.error["side_effect_status"] == "succeeded"
|
|
assert start.error["automatic_replay_allowed"] is False
|
|
assert service._acquisition_start_operation_id is None # noqa: SLF001
|
|
assert stop.stage_code == "physical-stop-classified-not-dispatched"
|
|
assert stop.error is not None
|
|
assert stop.error["side_effect_status"] == "none"
|
|
assert fixture.prepare_calls == [fixture.stop_operation_id]
|
|
assert fixture.control.stop_calls == 1
|
|
assert runtime.stop_calls == 1
|
|
|
|
|
|
@pytest.mark.parametrize("action", ["abort", "force", "reset", "close"])
|
|
def test_resolved_ready_dominates_later_local_action_without_rewriting_s0_or_pending_start(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
action: str,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(
|
|
service,
|
|
runtime,
|
|
confirm_first_pcl=False,
|
|
)
|
|
service.stop_acquisition(fixture.request)
|
|
owner = _settle_real_prepared_stop_before_publish(fixture)
|
|
_classify_real_prepared_stop(
|
|
fixture,
|
|
owner,
|
|
session_state="ready",
|
|
reconciliation_id=f"test-ready-dominates-{action}",
|
|
)
|
|
monkeypatch.setattr(service, "_seal_acquisition_capture_clock", lambda: None)
|
|
completed = service.state()
|
|
acquisition = service._acquisition # noqa: SLF001
|
|
assert acquisition is not None
|
|
assert completed["acquisition"]["state"] == "interrupted"
|
|
assert completed["acquisition"]["result"]["device_stop"] == "not-sent"
|
|
|
|
before_record = fixture.ledger.snapshot().record
|
|
before_acquisition = (
|
|
acquisition.state,
|
|
acquisition.state_revision,
|
|
acquisition.message_code,
|
|
dict(acquisition.result or {}),
|
|
)
|
|
before_operations = {
|
|
item["operation_id"]: item
|
|
for item in service._operations.snapshot() # noqa: SLF001
|
|
if item["operation_id"]
|
|
in {fixture.start_operation_id, fixture.stop_operation_id}
|
|
}
|
|
before_prepare_calls = list(fixture.prepare_calls)
|
|
before_control_stop_calls = fixture.control.stop_calls
|
|
|
|
if action == "abort":
|
|
result = service.abort_acquisition(
|
|
_abort_request(acquisition_id=acquisition.acquisition_id)
|
|
)
|
|
assert result["acquisition"]["state"] == "interrupted"
|
|
elif action == "force":
|
|
result = service.force_finish_acquisition_locally(
|
|
_force_finish_request(
|
|
acquisition_id=acquisition.acquisition_id,
|
|
expected_state_revision=acquisition.state_revision,
|
|
expected_recovery_generation=(
|
|
service._active_stream_recovery_generation # noqa: SLF001
|
|
),
|
|
)
|
|
)
|
|
assert result["acquisition"]["state"] == "interrupted"
|
|
elif action == "reset":
|
|
result = service.select_connection_mode(
|
|
DesiredConnectionModeRequest(
|
|
connection_mode="quick-connect",
|
|
expected_revision=service._desired_connection_mode_revision, # noqa: SLF001
|
|
reset_scenario=True,
|
|
reset_id="resolved-ready-reset-0001",
|
|
)
|
|
)
|
|
assert result["acquisition"]["state"] == "interrupted"
|
|
else:
|
|
service.close()
|
|
|
|
after_operations = {
|
|
item["operation_id"]: item
|
|
for item in service._operations.snapshot() # noqa: SLF001
|
|
if item["operation_id"]
|
|
in {fixture.start_operation_id, fixture.stop_operation_id}
|
|
}
|
|
after_record = fixture.ledger.snapshot().record
|
|
if action == "reset":
|
|
assert before_record is not None
|
|
assert after_record is not None
|
|
assert after_record.revision == before_record.revision + 1
|
|
assert after_record.resolution == "operator-retired-outcome-unknown"
|
|
assert after_record.original_command_outcome == "not-dispatched"
|
|
assert after_record.reconciliations == before_record.reconciliations
|
|
assert after_record.operator_retirements[-1].reason == (
|
|
"connection-scenario-reset-by-operator"
|
|
)
|
|
assert after_record.operator_retirements[-1].original_attempt.operation_id == (
|
|
before_record.operation_id
|
|
)
|
|
assert after_record.operator_retirements[-1].original_attempt.revision == (
|
|
before_record.revision
|
|
)
|
|
else:
|
|
assert after_record == before_record
|
|
assert (
|
|
acquisition.state,
|
|
acquisition.state_revision,
|
|
acquisition.message_code,
|
|
dict(acquisition.result or {}),
|
|
) == before_acquisition
|
|
assert after_operations == before_operations
|
|
assert before_operations[fixture.start_operation_id]["status"] == "interrupted"
|
|
assert before_operations[fixture.start_operation_id]["stage_code"] == (
|
|
"physical-start-active-but-no-point-before-standby"
|
|
)
|
|
assert before_operations[fixture.stop_operation_id]["stage_code"] == (
|
|
"physical-stop-classified-not-dispatched"
|
|
)
|
|
assert before_operations[fixture.stop_operation_id]["error"]["side_effect_status"] == "none"
|
|
assert fixture.prepare_calls == before_prepare_calls
|
|
assert fixture.control.stop_calls == before_control_stop_calls
|
|
|
|
|
|
@pytest.mark.parametrize("camera_outcome", ["healthy", "dead", "clean-ended"])
|
|
def test_classified_scanning_cancels_stop_eof_and_preserves_or_rearms_camera_epoch(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
camera_outcome: str,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
owner = _settle_real_prepared_stop_before_publish(fixture)
|
|
reconciled = _classify_real_prepared_stop(
|
|
fixture,
|
|
owner,
|
|
session_state="scanning",
|
|
reconciliation_id=f"test-scanning-camera-{camera_outcome}",
|
|
)
|
|
verified_binding = reconciled["reconciliations"][-1]["verified_binding"]["connection"]
|
|
fixture.control.state = "scanning"
|
|
fixture.control.state_revision += 1
|
|
fixture.control.failure = None
|
|
fixture.control.verified_control = {
|
|
**dict(verified_binding),
|
|
"logical_device_id": "known-k1",
|
|
"compatibility_profile_id": XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
}
|
|
service._device_session_id = f"fresh-camera-{camera_outcome}-session" # noqa: SLF001
|
|
# `_classify_real_prepared_stop` installs the fresh durable/control proof
|
|
# directly. Production Verify also publishes that same rotated route and
|
|
# DeviceInfo generation to the connection supervisor; mirror that portion
|
|
# here so the real camera activation admission remains exercised.
|
|
fresh_path = HostPathProbeResult(
|
|
available=True,
|
|
fingerprint=f"test-route:classified-camera:{camera_outcome}",
|
|
interface="test0",
|
|
source_ipv4="192.168.1.2",
|
|
route_class="direct",
|
|
kernel_route_fingerprint=(f"test-route:classified-camera:{camera_outcome}"),
|
|
)
|
|
fresh_host_epoch = service._connection_supervisor.observe_host_path( # noqa: SLF001
|
|
fresh_path
|
|
)
|
|
assert fresh_host_epoch == verified_binding["host_path_epoch"]
|
|
fresh_target = EndpointTarget(
|
|
str(verified_binding["target_ipv4"]),
|
|
int(verified_binding["target_port"]),
|
|
)
|
|
assert service._connection_supervisor.observe_endpoint( # noqa: SLF001
|
|
target=fresh_target,
|
|
intent_id=str(verified_binding["intent_id"]),
|
|
host_path_epoch=fresh_host_epoch,
|
|
reachable=True,
|
|
)
|
|
assert service._connection_supervisor.observe_control_evidence( # noqa: SLF001
|
|
VerifiedControlEvidence(
|
|
intent_id=str(verified_binding["intent_id"]),
|
|
transport_ref=str(verified_binding["transport_ref"]),
|
|
host_path_epoch=fresh_host_epoch,
|
|
target=fresh_target,
|
|
connection_mode=verified_binding["connection_mode"], # type: ignore[arg-type]
|
|
logical_device_id="known-k1",
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
control_session_id=str(verified_binding["control_session_id"]),
|
|
)
|
|
)
|
|
|
|
owner.out_dir.mkdir(parents=True, exist_ok=True)
|
|
initial_epoch = 5 if camera_outcome == "healthy" else None
|
|
camera_state: dict[str, object] = {
|
|
"phase": (
|
|
"streaming"
|
|
if camera_outcome == "healthy"
|
|
else "error"
|
|
if camera_outcome == "dead"
|
|
else "idle"
|
|
),
|
|
"generation": None if camera_outcome == "clean-ended" else 5,
|
|
"active_source_id": (
|
|
None
|
|
if camera_outcome == "clean-ended"
|
|
else facade_module.DEFAULT_ACQUISITION_CAMERA_SOURCE
|
|
),
|
|
# These are the real gateway shapes after producer detachment. The
|
|
# acquisition recording root stays active while both ended variants
|
|
# expose no active producer epoch. The dead case is the bounded
|
|
# pre-first-media decode failure which production admits for a fresh
|
|
# epoch; a generic source EOF must first finish its archive seal.
|
|
"recording": {
|
|
"active": True,
|
|
"session": owner.out_dir.name,
|
|
"active_epoch": initial_epoch,
|
|
"producer_alive": camera_outcome == "healthy",
|
|
"source_end_expected": False,
|
|
"committed_media_segment_count": 0,
|
|
},
|
|
"error": (
|
|
{"code": "invalid-fmp4", "message": "synthetic dead producer"}
|
|
if camera_outcome == "dead"
|
|
else None
|
|
),
|
|
}
|
|
cancel_calls: list[str] = []
|
|
select_calls: list[tuple[str, str]] = []
|
|
start_recording_calls: list[Path] = []
|
|
retry_calls: list[tuple[str, str, int, str]] = []
|
|
|
|
def snapshot_camera() -> dict[str, object]:
|
|
return {
|
|
**camera_state,
|
|
"recording": dict(camera_state["recording"]), # type: ignore[arg-type]
|
|
}
|
|
|
|
def cancel_expected_source_end() -> dict[str, object]:
|
|
cancel_calls.append("cancel")
|
|
return snapshot_camera()
|
|
|
|
def select_camera(source_id: str, target: str) -> dict[str, object]:
|
|
select_calls.append((source_id, target))
|
|
assert camera_outcome == "clean-ended"
|
|
recording = camera_state["recording"]
|
|
assert isinstance(recording, dict)
|
|
camera_state.update(
|
|
{
|
|
"phase": "connecting",
|
|
"generation": 6,
|
|
"active_source_id": source_id,
|
|
"error": None,
|
|
}
|
|
)
|
|
# A real select auto-spawns because the clean-ended gateway retained
|
|
# this acquisition's recording root.
|
|
recording.update({"active_epoch": 6, "producer_alive": True})
|
|
return snapshot_camera()
|
|
|
|
def start_recording(session_dir: Path) -> dict[str, object]:
|
|
start_recording_calls.append(session_dir)
|
|
assert session_dir == owner.out_dir
|
|
return snapshot_camera()
|
|
|
|
def retry_recording(
|
|
source_id: str,
|
|
target: str,
|
|
*,
|
|
expected_generation: int,
|
|
expected_recording_session: str,
|
|
pre_retry_fence: Callable[[Callable[[], bool]], bool],
|
|
commit_fence: Callable[[Callable[[], bool]], bool] | None = None,
|
|
committed_before_start: Callable[[dict[str, object]], None] | None = None,
|
|
) -> dict[str, object]:
|
|
retry_calls.append(
|
|
(source_id, target, expected_generation, expected_recording_session)
|
|
)
|
|
assert camera_outcome == "dead"
|
|
assert expected_generation == 5
|
|
assert expected_recording_session == owner.out_dir.name
|
|
assert pre_retry_fence(lambda: True) is True
|
|
recording = camera_state["recording"]
|
|
assert isinstance(recording, dict)
|
|
recording.update({"active_epoch": 6, "producer_alive": True})
|
|
camera_state.update(
|
|
{
|
|
"phase": "streaming",
|
|
"generation": 6,
|
|
"active_source_id": source_id,
|
|
"error": None,
|
|
}
|
|
)
|
|
assert commit_fence is not None
|
|
assert committed_before_start is not None
|
|
assert commit_fence(
|
|
lambda: committed_before_start(snapshot_camera()) is None
|
|
) is True
|
|
return snapshot_camera()
|
|
|
|
def activate_recording(
|
|
source_id: str,
|
|
target: str,
|
|
session_dir: Path,
|
|
*,
|
|
commit_fence: Callable[[Callable[[], bool]], bool],
|
|
committed_before_start: Callable[[dict[str, object]], None] | None = None,
|
|
) -> dict[str, object]:
|
|
select_camera(source_id, target)
|
|
start_recording(session_dir)
|
|
assert committed_before_start is not None
|
|
assert commit_fence(
|
|
lambda: committed_before_start(snapshot_camera()) is None
|
|
) is True
|
|
return snapshot_camera()
|
|
|
|
monkeypatch.setattr(service.camera_preview, "snapshot", snapshot_camera)
|
|
monkeypatch.setattr(
|
|
service.camera_preview,
|
|
"cancel_expected_source_end",
|
|
cancel_expected_source_end,
|
|
)
|
|
monkeypatch.setattr(service.camera_preview, "select", select_camera)
|
|
monkeypatch.setattr(service.camera_preview, "start_recording", start_recording)
|
|
monkeypatch.setattr(
|
|
service.camera_preview,
|
|
"activate_recording_producer",
|
|
activate_recording,
|
|
)
|
|
monkeypatch.setattr(
|
|
service.camera_preview,
|
|
"retry_recording_producer",
|
|
retry_recording,
|
|
)
|
|
activation_lineage = (
|
|
owner.lineage.acquisition_id,
|
|
owner.out_dir.name,
|
|
owner.runtime_producer_generation,
|
|
)
|
|
with service._lock: # noqa: SLF001
|
|
service._camera_activation_lineage = activation_lineage # noqa: SLF001
|
|
|
|
service._adopt_classified_prepared_stop_scanning_in_place( # noqa: SLF001
|
|
owner=owner,
|
|
reconciliation_id=f"test-scanning-camera-{camera_outcome}",
|
|
reconciled_record=reconciled,
|
|
)
|
|
|
|
assert cancel_calls == ["cancel"]
|
|
assert snapshot_camera()["recording"]["source_end_expected"] is False # type: ignore[index]
|
|
assert select_calls == []
|
|
assert start_recording_calls == []
|
|
assert retry_calls == []
|
|
with service._lock: # noqa: SLF001
|
|
if camera_outcome == "healthy":
|
|
assert service._camera_activation_lineage == activation_lineage # noqa: SLF001
|
|
assert service._camera_activation_retry_lineage is None # noqa: SLF001
|
|
else:
|
|
assert service._camera_activation_lineage is None # noqa: SLF001
|
|
assert service._camera_activation_retry_lineage == activation_lineage # noqa: SLF001
|
|
assert snapshot_camera()["recording"]["active_epoch"] == initial_epoch # type: ignore[index]
|
|
|
|
checkpoint_store = service._active_acquisition_checkpoint # noqa: SLF001
|
|
assert checkpoint_store is not None
|
|
pending_before_pcl = service._classified_stop_rebind_pending # noqa: SLF001
|
|
assert pending_before_pcl is not None
|
|
gap_snapshot = checkpoint_store.snapshot()
|
|
assert gap_snapshot.status == "active"
|
|
assert gap_snapshot.checkpoint is not None
|
|
assert gap_snapshot.checkpoint.last_gap_started_at_utc is not None
|
|
assert gap_snapshot.checkpoint.last_gap_recovered_at_utc is None
|
|
original_lease = service._acquisition_session_lease # noqa: SLF001
|
|
original_out_dir = service._acquisition_out_dir # noqa: SLF001
|
|
|
|
frame = DecodedPointCloudView(
|
|
context=ConsumerFrameContext(
|
|
sequence=1,
|
|
captured_at_epoch_ns=1,
|
|
received_monotonic_ns=1,
|
|
processing_started_monotonic_ns=1,
|
|
encoded_size_bytes=16,
|
|
live=True,
|
|
),
|
|
frame_id="map",
|
|
positions_xyz=((0.0, 0.0, 0.0),),
|
|
)
|
|
runtime.phase = "live"
|
|
runtime.source_mode = "live"
|
|
runtime.source_ready = True
|
|
runtime.pcl_frames = 1
|
|
stale_frame = DecodedPointCloudView(
|
|
context=ConsumerFrameContext(
|
|
sequence=2,
|
|
captured_at_epoch_ns=1,
|
|
received_monotonic_ns=1,
|
|
processing_started_monotonic_ns=1,
|
|
encoded_size_bytes=16,
|
|
live=False,
|
|
),
|
|
frame_id="map",
|
|
positions_xyz=((0.0, 0.0, 0.0),),
|
|
)
|
|
empty_frame = DecodedPointCloudView(
|
|
context=ConsumerFrameContext(
|
|
sequence=1,
|
|
captured_at_epoch_ns=1,
|
|
received_monotonic_ns=1,
|
|
processing_started_monotonic_ns=1,
|
|
encoded_size_bytes=0,
|
|
live=True,
|
|
),
|
|
frame_id="map",
|
|
positions_xyz=(),
|
|
)
|
|
service._observe_published_runtime_envelope( # noqa: SLF001
|
|
stale_frame,
|
|
runtime.producer_generation,
|
|
)
|
|
service._observe_published_runtime_envelope( # noqa: SLF001
|
|
empty_frame,
|
|
runtime.producer_generation,
|
|
)
|
|
service._observe_published_runtime_envelope( # noqa: SLF001
|
|
frame,
|
|
runtime.producer_generation + 1,
|
|
)
|
|
with service._camera_restart_commit_gate, service._lock: # noqa: SLF001
|
|
service._camera_stop_priority_counts[ # noqa: SLF001
|
|
owner.lineage.acquisition_id
|
|
] = 1
|
|
try:
|
|
assert service._promote_classified_stop_rebind_from_pcl( # noqa: SLF001
|
|
envelope=frame,
|
|
producer_generation=runtime.producer_generation,
|
|
) is False
|
|
finally:
|
|
with service._camera_restart_commit_gate, service._lock: # noqa: SLF001
|
|
service._camera_stop_priority_counts.pop( # noqa: SLF001
|
|
owner.lineage.acquisition_id,
|
|
None,
|
|
)
|
|
assert service._classified_stop_rebind_pending == pending_before_pcl # noqa: SLF001
|
|
assert service._classified_stop_rebind_inflight is None # noqa: SLF001
|
|
assert checkpoint_store.snapshot().checkpoint == gap_snapshot.checkpoint
|
|
assert select_calls == []
|
|
assert start_recording_calls == []
|
|
assert retry_calls == []
|
|
|
|
rebind_calls: list[str] = []
|
|
original_rebind_active = checkpoint_store.rebind_active
|
|
|
|
def observe_rebind_active(**kwargs: Any) -> Any:
|
|
rebind_calls.append(str(kwargs["transition_id"]))
|
|
committed = original_rebind_active(**kwargs)
|
|
if camera_outcome == "dead":
|
|
raise OSError("synthetic lost response after durable rebind")
|
|
return committed
|
|
|
|
monkeypatch.setattr(
|
|
checkpoint_store,
|
|
"rebind_active",
|
|
observe_rebind_active,
|
|
)
|
|
service._observe_published_runtime_envelope( # noqa: SLF001
|
|
frame,
|
|
runtime.producer_generation,
|
|
)
|
|
committed_snapshot = checkpoint_store.snapshot()
|
|
assert committed_snapshot.status == "active"
|
|
assert committed_snapshot.checkpoint is not None
|
|
assert committed_snapshot.checkpoint.revision == (
|
|
gap_snapshot.checkpoint.revision + 1
|
|
)
|
|
assert committed_snapshot.checkpoint.last_gap_recovered_at_utc is not None
|
|
assert rebind_calls == [pending_before_pcl.transition_id]
|
|
assert service._classified_stop_rebind_pending is None # noqa: SLF001
|
|
assert service._classified_stop_rebind_inflight is None # noqa: SLF001
|
|
assert service._acquisition_session_lease is original_lease # noqa: SLF001
|
|
assert service._acquisition_out_dir == original_out_dir # noqa: SLF001
|
|
deadline = time.monotonic() + 2.0
|
|
while (
|
|
camera_outcome != "healthy"
|
|
and not (select_calls or retry_calls)
|
|
and time.monotonic() < deadline
|
|
):
|
|
time.sleep(0.01)
|
|
service._observe_published_runtime_envelope( # noqa: SLF001
|
|
frame,
|
|
runtime.producer_generation,
|
|
)
|
|
time.sleep(0.05)
|
|
assert checkpoint_store.snapshot().checkpoint == committed_snapshot.checkpoint
|
|
assert rebind_calls == [pending_before_pcl.transition_id]
|
|
assert len(select_calls) + len(retry_calls) <= 1
|
|
|
|
if camera_outcome == "healthy":
|
|
assert select_calls == []
|
|
assert start_recording_calls == []
|
|
assert retry_calls == []
|
|
assert snapshot_camera()["recording"]["active_epoch"] == initial_epoch # type: ignore[index]
|
|
elif camera_outcome == "clean-ended":
|
|
assert select_calls == [
|
|
(
|
|
facade_module.DEFAULT_ACQUISITION_CAMERA_SOURCE,
|
|
owner.lineage.target_ipv4,
|
|
)
|
|
]
|
|
assert start_recording_calls == [owner.out_dir]
|
|
assert retry_calls == []
|
|
assert snapshot_camera()["recording"]["active_epoch"] == 6 # type: ignore[index]
|
|
else:
|
|
assert select_calls == []
|
|
assert start_recording_calls == []
|
|
assert retry_calls == [
|
|
(
|
|
facade_module.DEFAULT_ACQUISITION_CAMERA_SOURCE,
|
|
owner.lineage.target_ipv4,
|
|
5,
|
|
owner.out_dir.name,
|
|
)
|
|
]
|
|
assert snapshot_camera()["recording"]["active_epoch"] == 6 # type: ignore[index]
|
|
|
|
|
|
def test_classified_ready_cleanup_failure_retains_owner_then_retries_locally(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
owner = _settle_real_prepared_stop_before_publish(fixture)
|
|
_classify_real_prepared_stop(
|
|
fixture,
|
|
owner,
|
|
session_state="ready",
|
|
reconciliation_id="test-ready-cleanup-retry",
|
|
)
|
|
monkeypatch.setattr(service, "_seal_acquisition_capture_clock", lambda: None)
|
|
original_stop_current = service.camera_preview.stop_current
|
|
failure_pending = True
|
|
|
|
def fail_once_stop_current() -> dict[str, object]:
|
|
nonlocal failure_pending
|
|
if failure_pending:
|
|
failure_pending = False
|
|
raise RuntimeError("synthetic camera current cleanup failure")
|
|
return original_stop_current()
|
|
|
|
monkeypatch.setattr(service.camera_preview, "stop_current", fail_once_stop_current)
|
|
|
|
with pytest.raises(RuntimeError, match="synthetic camera current cleanup failure"):
|
|
service.state()
|
|
failed = service._acquisition # noqa: SLF001
|
|
assert failed is not None
|
|
assert failed.state == "failed"
|
|
assert failed.result is not None
|
|
assert failed.result["device_stop"] == "not-sent"
|
|
assert failed.result["local_cleanup_retry_pending"] is True
|
|
assert service._acquisition_session_lease is owner.session_lease # noqa: SLF001
|
|
assert service._prepared_stop_recovery_owner is owner # noqa: SLF001
|
|
assert runtime.stop_calls == 0
|
|
|
|
retried = service.state()
|
|
assert retried["acquisition"]["state"] == "failed"
|
|
assert retried["acquisition"]["cleanup_pending"] is False
|
|
assert retried["acquisition"]["result"]["device_stop"] == "not-sent"
|
|
assert retried["acquisition"]["result"]["receiver_stopped"] is True
|
|
assert "local_cleanup_retry_pending" not in retried["acquisition"]["result"]
|
|
assert retried["acquisition"]["message_code"] == (
|
|
"acquisition.recovery.local_cleanup_completed"
|
|
)
|
|
assert failed.result is not None
|
|
assert failed.result["receiver_stopped"] is True
|
|
assert "local_cleanup_retry_pending" not in failed.result
|
|
assert service._acquisition_session_lease is None # noqa: SLF001
|
|
assert service._prepared_stop_recovery_owner is None # noqa: SLF001
|
|
assert runtime.stop_calls == 1
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None
|
|
assert record.operation_id == fixture.stop_operation_id
|
|
assert record.resolution == "not-dispatched"
|
|
assert fixture.control.stop_calls == 1
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("failure_stage", "expected_message_code"),
|
|
[
|
|
(
|
|
"camera-process-lease",
|
|
"acquisition.recovery.camera_fence_release_failed",
|
|
),
|
|
("runtime", "acquisition.recovery.runtime_cleanup_failed"),
|
|
("capture-clock", "acquisition.recovery.capture_clock_failed"),
|
|
(
|
|
"perception-ingress",
|
|
"acquisition.recovery.perception_ingress_cleanup_failed",
|
|
),
|
|
(
|
|
"evidence-lease",
|
|
"acquisition.recovery.evidence_lease_release_failed",
|
|
),
|
|
],
|
|
)
|
|
def test_classified_ready_staged_cleanup_failure_retains_exact_owner_for_local_retry(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
failure_stage: str,
|
|
expected_message_code: str,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
owner = _settle_real_prepared_stop_before_publish(fixture)
|
|
_classify_real_prepared_stop(
|
|
fixture,
|
|
owner,
|
|
session_state="ready",
|
|
reconciliation_id=f"test-ready-{failure_stage}",
|
|
)
|
|
activation_lineage = (
|
|
owner.lineage.acquisition_id,
|
|
owner.out_dir.name,
|
|
owner.runtime_producer_generation,
|
|
)
|
|
with service._lock: # noqa: SLF001
|
|
service._live_perception_camera_binding = ( # noqa: SLF001
|
|
owner.out_dir.name,
|
|
facade_module.DEFAULT_ACQUISITION_CAMERA_SOURCE,
|
|
1,
|
|
)
|
|
service._camera_activation_lineage = activation_lineage # noqa: SLF001
|
|
service._camera_activation_retry_lineage = activation_lineage # noqa: SLF001
|
|
|
|
failure_pending = True
|
|
process_lease = None
|
|
if failure_stage == "camera-process-lease":
|
|
assert service._ensure_camera_preview_process_lease() is True # noqa: SLF001
|
|
process_lease = service._application_control_process_lease # noqa: SLF001
|
|
assert process_lease is not None
|
|
original_release = type(process_lease).release
|
|
|
|
def fail_once_process_release(candidate: object) -> object:
|
|
nonlocal failure_pending
|
|
if candidate is process_lease and failure_pending:
|
|
failure_pending = False
|
|
raise RuntimeError("synthetic camera process lease release failure")
|
|
return original_release(candidate) # type: ignore[arg-type]
|
|
|
|
monkeypatch.setattr(type(process_lease), "release", fail_once_process_release)
|
|
elif failure_stage == "runtime":
|
|
runtime.stop_error = RuntimeError("synthetic recovery runtime stop failure")
|
|
elif failure_stage == "capture-clock":
|
|
original_seal = service._seal_acquisition_capture_clock # noqa: SLF001
|
|
|
|
def fail_once_clock() -> None:
|
|
nonlocal failure_pending
|
|
if failure_pending:
|
|
failure_pending = False
|
|
raise RuntimeError("synthetic recovery capture clock failure")
|
|
original_seal()
|
|
|
|
monkeypatch.setattr(service, "_seal_acquisition_capture_clock", fail_once_clock)
|
|
elif failure_stage == "perception-ingress":
|
|
original_end_session = service.live_perception_ingress.end_session
|
|
|
|
def fail_once_ingress(session_id: str) -> None:
|
|
nonlocal failure_pending
|
|
if failure_pending:
|
|
failure_pending = False
|
|
raise RuntimeError("synthetic recovery ingress cleanup failure")
|
|
original_end_session(session_id)
|
|
|
|
monkeypatch.setattr(
|
|
service.live_perception_ingress,
|
|
"end_session",
|
|
fail_once_ingress,
|
|
)
|
|
elif failure_stage == "evidence-lease":
|
|
original_release_evidence = service._release_acquisition_session_lease # noqa: SLF001
|
|
|
|
def fail_once_evidence_release() -> None:
|
|
nonlocal failure_pending
|
|
if failure_pending:
|
|
failure_pending = False
|
|
raise RuntimeError("synthetic recovery evidence lease release failure")
|
|
original_release_evidence()
|
|
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_release_acquisition_session_lease",
|
|
fail_once_evidence_release,
|
|
)
|
|
else: # pragma: no cover - exhaustive test table
|
|
raise AssertionError(failure_stage)
|
|
|
|
with pytest.raises(RuntimeError, match="synthetic"):
|
|
service.state()
|
|
|
|
failed = service._acquisition # noqa: SLF001
|
|
assert failed is owner.acquisition
|
|
assert failed.state == "failed"
|
|
assert failed.message_code == expected_message_code
|
|
assert failed.result is not None
|
|
assert failed.result["device_stop"] == "not-sent"
|
|
assert failed.result["local_cleanup_retry_pending"] is True
|
|
assert failed.result["local_cleanup_stage"] == failure_stage
|
|
assert failed.result["local_cleanup_error_code"] == "RuntimeError"
|
|
assert service._acquisition_session_lease is owner.session_lease # noqa: SLF001
|
|
assert service._prepared_stop_recovery_owner is owner # noqa: SLF001
|
|
if failure_stage == "camera-process-lease":
|
|
assert runtime.stop_calls == 0
|
|
assert service._application_control_process_lease is process_lease # noqa: SLF001
|
|
assert service._application_control_process_lease_holders == { # noqa: SLF001
|
|
"camera"
|
|
}
|
|
|
|
runtime.stop_error = None
|
|
retried = service.state()
|
|
|
|
assert retried["acquisition"]["state"] == "failed"
|
|
assert retried["acquisition"]["cleanup_pending"] is False
|
|
assert retried["acquisition"]["result"]["device_stop"] == "not-sent"
|
|
assert retried["acquisition"]["result"]["receiver_stopped"] is True
|
|
assert "local_cleanup_retry_pending" not in retried["acquisition"]["result"]
|
|
assert "local_cleanup_stage" not in retried["acquisition"]["result"]
|
|
assert retried["acquisition"]["message_code"] == (
|
|
"acquisition.recovery.local_cleanup_completed"
|
|
)
|
|
assert service._acquisition_session_lease is None # noqa: SLF001
|
|
assert service._prepared_stop_recovery_owner is None # noqa: SLF001
|
|
assert service._application_control_process_lease_holders == set() # noqa: SLF001
|
|
assert service._application_control_process_lease is None # noqa: SLF001
|
|
assert service._live_perception_camera_binding is None # noqa: SLF001
|
|
assert service._camera_activation_lineage is None # noqa: SLF001
|
|
assert service._camera_activation_retry_lineage is None # noqa: SLF001
|
|
assert fixture.control.stop_calls == 1
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None
|
|
assert record.operation_id == fixture.stop_operation_id
|
|
assert record.resolution == "not-dispatched"
|
|
|
|
|
|
def test_classified_ready_ambiguous_process_fence_release_requires_restart(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
owner = _settle_real_prepared_stop_before_publish(fixture)
|
|
_classify_real_prepared_stop(
|
|
fixture,
|
|
owner,
|
|
session_state="ready",
|
|
reconciliation_id="test-ready-ambiguous-process-fence",
|
|
)
|
|
assert service._ensure_camera_preview_process_lease() is True # noqa: SLF001
|
|
process_lease = service._application_control_process_lease # noqa: SLF001
|
|
assert process_lease is not None
|
|
descriptor = process_lease._descriptor # noqa: SLF001
|
|
original_flock = fcntl.flock
|
|
original_close = os.close
|
|
unlock_calls = 0
|
|
close_calls = 0
|
|
|
|
def fail_unlock(candidate: int, operation: int) -> None:
|
|
nonlocal unlock_calls
|
|
if candidate == descriptor and operation == fcntl.LOCK_UN:
|
|
unlock_calls += 1
|
|
raise OSError(5, "synthetic unlock failure")
|
|
original_flock(candidate, operation)
|
|
|
|
def fail_close(candidate: int) -> None:
|
|
nonlocal close_calls
|
|
if candidate == descriptor:
|
|
close_calls += 1
|
|
raise OSError(5, "synthetic close failure")
|
|
original_close(candidate)
|
|
|
|
monkeypatch.setattr(fcntl, "flock", fail_unlock)
|
|
monkeypatch.setattr(os, "close", fail_close)
|
|
try:
|
|
with pytest.raises(
|
|
facade_module.ApplicationControlProcessLeaseReleaseAmbiguous
|
|
):
|
|
service.state()
|
|
|
|
failed = service._acquisition # noqa: SLF001
|
|
assert failed is owner.acquisition
|
|
assert failed.state == "failed"
|
|
assert failed.result is not None
|
|
assert failed.result["local_cleanup_retry_pending"] is True
|
|
assert failed.result["local_cleanup_stage"] == "camera-process-lease"
|
|
assert service._acquisition_session_lease is owner.session_lease # noqa: SLF001
|
|
assert service._prepared_stop_recovery_owner is owner # noqa: SLF001
|
|
assert runtime.stop_calls == 0
|
|
assert (unlock_calls, close_calls) == (1, 1)
|
|
|
|
quarantined = service.state()
|
|
|
|
assert quarantined["acquisition"]["cleanup_pending"] is True
|
|
assert quarantined["acquisition"]["result"][
|
|
"local_cleanup_retry_pending"
|
|
] is True
|
|
assert quarantined["k1_lifecycle_process_lease"] == {
|
|
"held_by_current_service": False,
|
|
"holders": [],
|
|
"process_lease_quarantined": True,
|
|
"reason_code": (
|
|
"application-control-process-lease-release-ambiguous"
|
|
),
|
|
"restart_required": True,
|
|
}
|
|
assert quarantined["connection_policy"]["allowed_actions"] == []
|
|
assert quarantined["connection_policy"]["recommended_action"] == (
|
|
"restart-mission-core"
|
|
)
|
|
assert (unlock_calls, close_calls) == (1, 1)
|
|
assert runtime.stop_calls == 0
|
|
with pytest.raises(
|
|
facade_module.ApplicationControlProcessLeaseUnavailable
|
|
):
|
|
service._ensure_camera_preview_process_lease() # noqa: SLF001
|
|
finally:
|
|
monkeypatch.setattr(fcntl, "flock", original_flock)
|
|
monkeypatch.setattr(os, "close", original_close)
|
|
original_flock(descriptor, fcntl.LOCK_UN)
|
|
original_close(descriptor)
|
|
|
|
|
|
def test_repeated_classified_stop_chain_preserves_pending_start_until_first_pcl(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(
|
|
service,
|
|
runtime,
|
|
confirm_first_pcl=False,
|
|
)
|
|
start_operation = service._operations.get(fixture.start_operation_id) # noqa: SLF001
|
|
assert start_operation.status == "running"
|
|
assert service._acquisition_start_operation_id == fixture.start_operation_id # noqa: SLF001
|
|
|
|
service.stop_acquisition(fixture.request)
|
|
owner0 = _settle_real_prepared_stop_before_publish(fixture)
|
|
reconciled0 = _classify_real_prepared_stop(
|
|
fixture,
|
|
owner0,
|
|
session_state="scanning",
|
|
reconciliation_id="test-chain-s0",
|
|
)
|
|
binding0 = reconciled0["reconciliations"][-1]["verified_binding"]["connection"]
|
|
fixture.control.state = "scanning"
|
|
fixture.control.state_revision += 1
|
|
fixture.control.failure = None
|
|
fixture.control.verified_control = {
|
|
**dict(binding0),
|
|
"logical_device_id": "known-k1",
|
|
"compatibility_profile_id": XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
}
|
|
service._device_session_id = "fresh-chain-s0-session" # noqa: SLF001
|
|
service._adopt_classified_prepared_stop_scanning_in_place( # noqa: SLF001
|
|
owner=owner0,
|
|
reconciliation_id="test-chain-s0",
|
|
reconciled_record=reconciled0,
|
|
)
|
|
assert service._acquisition.state == "awaiting_external_start" # noqa: SLF001
|
|
assert service._acquisition_start_operation_id == fixture.start_operation_id # noqa: SLF001
|
|
|
|
stop1_id = "op-00000000-0000-4000-8000-000000001403"
|
|
service.stop_acquisition(
|
|
_stop_request(
|
|
acquisition_id=owner0.lineage.acquisition_id,
|
|
operation_id=stop1_id,
|
|
idempotency_key="classified-chain-s1",
|
|
mode="graceful",
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
expected_control_session_generation=fixture.control.session_generation,
|
|
expected_control_state_revision=fixture.control.state_revision,
|
|
)
|
|
)
|
|
fixture.control.state = "failed"
|
|
fixture.control.state_revision += 1
|
|
fixture.control.failure = {
|
|
"reason_code": "application-connection-binding-lost",
|
|
"modeling_command_attempted": False,
|
|
"safe_to_retry": False,
|
|
}
|
|
service.state()
|
|
owner1 = service._prepared_stop_recovery_owner # noqa: SLF001
|
|
assert isinstance(owner1, facade_module._PreparedStopRecoveryOwner)
|
|
assert owner1.lineage.stop_operation_id == stop1_id
|
|
assert owner1.lineage.parent_physical_operation_id == fixture.stop_operation_id
|
|
assert owner1.start_operation_id == fixture.start_operation_id
|
|
|
|
reconciled1 = _classify_real_prepared_stop(
|
|
fixture,
|
|
owner1,
|
|
session_state="scanning",
|
|
reconciliation_id="test-chain-s1",
|
|
)
|
|
binding1 = reconciled1["reconciliations"][-1]["verified_binding"]["connection"]
|
|
fixture.control.state = "scanning"
|
|
fixture.control.state_revision += 1
|
|
fixture.control.failure = None
|
|
fixture.control.verified_control = {
|
|
**dict(binding1),
|
|
"logical_device_id": "known-k1",
|
|
"compatibility_profile_id": XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
}
|
|
with service._lock: # noqa: SLF001
|
|
service._selected_device_id = fixture.binding.transport_ref # noqa: SLF001
|
|
service._connection_mode = fixture.binding.connection_mode # noqa: SLF001
|
|
service._k1_ip = fixture.binding.target_ipv4 # noqa: SLF001
|
|
service._device_session_id = "fresh-chain-s1-session" # noqa: SLF001
|
|
service._adopt_classified_prepared_stop_scanning_in_place( # noqa: SLF001
|
|
owner=owner1,
|
|
reconciliation_id="test-chain-s1",
|
|
reconciled_record=reconciled1,
|
|
)
|
|
assert service._matching_start_active_confirmed( # noqa: SLF001
|
|
fixture.coordinator.snapshot(),
|
|
acquisition_id=owner1.lineage.acquisition_id,
|
|
start_operation_id=fixture.start_operation_id,
|
|
verified_control=fixture.control.verified_control,
|
|
)
|
|
|
|
repeated_s0 = service.stop_acquisition(fixture.request)
|
|
current_after_old_retry = fixture.ledger.snapshot().record
|
|
assert current_after_old_retry is not None
|
|
assert current_after_old_retry.operation_id == stop1_id
|
|
assert current_after_old_retry.stage == "resolved"
|
|
assert repeated_s0["acquisition"]["state"] == "awaiting_external_start"
|
|
assert fixture.prepare_calls == [fixture.stop_operation_id, stop1_id]
|
|
|
|
runtime.phase = "live"
|
|
runtime.source_ready = True
|
|
runtime.recovery_state = "recovered"
|
|
runtime.pcl_frames = 1
|
|
first_pcl = service.state()
|
|
start_operation = service._operations.get(fixture.start_operation_id) # noqa: SLF001
|
|
assert first_pcl["acquisition"]["state"] == "acquiring"
|
|
assert start_operation.status == "succeeded"
|
|
assert start_operation.stage_code == "canonical-start-and-first-point-frame"
|
|
assert service._acquisition_start_operation_id is None # noqa: SLF001
|
|
with service._lock: # noqa: SLF001
|
|
service._active_stream_recovery_lineage = None # noqa: SLF001
|
|
service._active_stream_recovery_state = "inactive" # noqa: SLF001
|
|
runtime.phase = "reconnecting"
|
|
runtime.source_ready = False
|
|
runtime.recovery_state = "reconnecting"
|
|
second_disconnect = service._admit_active_stream_recovery_lineage( # noqa: SLF001
|
|
runtime=runtime.snapshot(),
|
|
)
|
|
assert second_disconnect is not None
|
|
assert second_disconnect.physical_operation_id == stop1_id
|
|
assert second_disconnect.start_operation_id == fixture.start_operation_id
|
|
runtime.phase = "live"
|
|
runtime.source_ready = True
|
|
runtime.recovery_state = "recovered"
|
|
current_physical = fixture.coordinator.snapshot()
|
|
current_record = current_physical["record"]
|
|
assert isinstance(current_record, dict)
|
|
assert service._physical_active_parent_proof( # noqa: SLF001
|
|
current_physical,
|
|
acquisition_id=owner1.lineage.acquisition_id,
|
|
parent_operation_id=str(current_record["operation_id"]),
|
|
verified_control=fixture.control.verified_control,
|
|
) is not None
|
|
|
|
stop2_id = "op-00000000-0000-4000-8000-000000001404"
|
|
captured_stop2 = service._capture_prepared_stop_dispatch_lineage( # noqa: SLF001
|
|
operation_id=stop2_id,
|
|
acquisition=owner1.acquisition,
|
|
)
|
|
assert captured_stop2 is not None
|
|
service.stop_acquisition(
|
|
_stop_request(
|
|
acquisition_id=owner1.lineage.acquisition_id,
|
|
operation_id=stop2_id,
|
|
idempotency_key="classified-chain-s2",
|
|
mode="graceful",
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
expected_control_session_generation=fixture.control.session_generation,
|
|
expected_control_state_revision=fixture.control.state_revision,
|
|
)
|
|
)
|
|
current = fixture.ledger.snapshot().record
|
|
assert current is not None
|
|
assert current.operation_id == stop2_id
|
|
assert current.parent_operation_id == stop1_id
|
|
assert current.stage == "prepared"
|
|
|
|
|
|
def test_prepared_stop_gate_defer_preserves_terminal_truth_until_settlement_and_teardown(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
fixture.control.state = "failed"
|
|
fixture.control.state_revision += 1
|
|
fixture.control.failure = {
|
|
"reason_code": "mqtt_network_loop_failed",
|
|
"modeling_command_attempted": False,
|
|
"safe_to_retry": False,
|
|
}
|
|
|
|
# Simulate the transport owning its global dispatch fence. Both truthful
|
|
# settlement and topology retirement defer; generic reducers must not
|
|
# convert the still-PREPARED STOP to UNKNOWN in the meantime.
|
|
assert service._k1_command_dispatch_gate.acquire(blocking=False) # noqa: SLF001
|
|
try:
|
|
deferred = service.state()
|
|
finally:
|
|
service._k1_command_dispatch_gate.release() # noqa: SLF001
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.status == "running"
|
|
assert operation.error is None
|
|
assert deferred["selected_device_id"] == fixture.binding.transport_ref
|
|
assert deferred["application_control_session"]["state"] == "failed"
|
|
|
|
# On the next poll, settlement happens before proven-loss retirement could
|
|
# reset the failed control owner to idle. Exact NONE instead promotes the
|
|
# retained receiver into read-only classification recovery, so teardown is
|
|
# intentionally deferred rather than discarding the live evidence owner.
|
|
settled = service.state()
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.status == "failed"
|
|
assert operation.stage_code == "physical-stop-classified-not-dispatched"
|
|
assert operation.error is not None
|
|
assert operation.error["side_effect_status"] == "none"
|
|
assert operation.error["physical_command_sent"] is False
|
|
assert settled["selected_device_id"] == fixture.binding.transport_ref
|
|
assert service._prepared_stop_recovery_owner is not None # noqa: SLF001
|
|
assert settled["application_control_session"]["state"] == "failed"
|
|
|
|
|
|
def test_stop_without_exact_parent_proof_fails_before_durable_prepare(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
parent = fixture.ledger.snapshot().record
|
|
assert parent is not None
|
|
original_snapshot = fixture.coordinator.snapshot
|
|
|
|
def snapshot_without_parent_identity() -> dict[str, object]:
|
|
snapshot = original_snapshot()
|
|
record = snapshot.get("record")
|
|
assert isinstance(record, dict)
|
|
snapshot["record"] = {**record, "identity": None}
|
|
return snapshot
|
|
|
|
monkeypatch.setattr(fixture.coordinator, "snapshot", snapshot_without_parent_identity)
|
|
with pytest.raises(
|
|
facade_module.LocalAcquisitionLifecycleError,
|
|
match="physical lineage",
|
|
) as raised:
|
|
service.stop_acquisition(fixture.request)
|
|
|
|
assert raised.value.reason_code == "physical-stop-parent-proof-unavailable"
|
|
assert fixture.prepare_calls == []
|
|
assert fixture.control.stop_calls == 0
|
|
current = fixture.ledger.snapshot().record
|
|
assert current is not None
|
|
assert current.operation_id == parent.operation_id
|
|
assert current.stage == "resolved"
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.status == "failed"
|
|
assert operation.error is not None
|
|
assert operation.error["side_effect_status"] == "none"
|
|
|
|
|
|
@dataclass
|
|
class _ActualWorkerStopTransportSnapshot:
|
|
state: str
|
|
publish_attempts: int
|
|
|
|
def as_dict(self) -> dict[str, object]:
|
|
return {
|
|
"state": self.state,
|
|
"publish_attempts": self.publish_attempts,
|
|
"qos2_completions": 0,
|
|
"correlated_responses": 1,
|
|
"ignored_known_responses": 0,
|
|
"late_known_responses": 0,
|
|
"latest_device_session_state": "scanning",
|
|
"latest_device_project_bound": True,
|
|
"latest_device_init_ready": True,
|
|
"automatic_retry": False,
|
|
"automatic_reconnect": False,
|
|
"control_proof_revision": 1,
|
|
"control_proof_source": "correlated-application-response",
|
|
"control_proof_fresh": True,
|
|
"control_proof_age_seconds": 0.0,
|
|
}
|
|
|
|
|
|
class _ActualWorkerStopRaceTransport:
|
|
"""Fake MQTT client boundary driven by the real control-session worker."""
|
|
|
|
def __init__(self) -> None:
|
|
self.state = "new"
|
|
self.publish_attempts = 0
|
|
self.fake_client_publish_calls: list[str] = []
|
|
self.evidence_observer: object | None = None
|
|
self.dispatch_guard: Callable[[], Callable[[], None] | None] | None = None
|
|
self.before_transport_guard = threading.Event()
|
|
self.release_transport_guard = threading.Event()
|
|
self.after_mark_dispatching = threading.Event()
|
|
self.release_fake_publish = threading.Event()
|
|
|
|
def install_evidence_observer(self, observer: object) -> None:
|
|
self.evidence_observer = observer
|
|
|
|
def install_dispatch_guard(
|
|
self,
|
|
guard: Callable[[], Callable[[], None] | None],
|
|
) -> None:
|
|
self.dispatch_guard = guard
|
|
|
|
def open(self) -> _ActualWorkerStopTransportSnapshot:
|
|
self.state = "ready"
|
|
return self.snapshot()
|
|
|
|
def close(self) -> None:
|
|
self.state = "closed"
|
|
|
|
def snapshot(self) -> _ActualWorkerStopTransportSnapshot:
|
|
return _ActualWorkerStopTransportSnapshot(self.state, self.publish_attempts)
|
|
|
|
def validate_control_proof(self, _binding: LiveDeviceControlBinding) -> None:
|
|
return None
|
|
|
|
def scan_initialization_complete(self, _binding: LiveDeviceControlBinding) -> bool:
|
|
return True
|
|
|
|
def exchange_batch_once(
|
|
self,
|
|
envelopes: Any,
|
|
*,
|
|
required_response_operation_keys: Any,
|
|
) -> dict[str, bytes]:
|
|
envelope = tuple(envelopes)[0]
|
|
assert envelope.operation_key == "modeling:stop"
|
|
assert required_response_operation_keys == {"modeling:stop"}
|
|
assert self.dispatch_guard is not None
|
|
release = self.dispatch_guard()
|
|
try:
|
|
self.publish_attempts += 1
|
|
observer = self.evidence_observer
|
|
assert observer is not None
|
|
observer.publish_dispatching(
|
|
ApplicationMqttPublishEvidence(
|
|
operation_key="modeling:stop",
|
|
topic=envelope.topic,
|
|
payload_sha256=envelope.payload_sha256,
|
|
qos=envelope.qos,
|
|
retain=envelope.retain,
|
|
packet_id=None,
|
|
)
|
|
)
|
|
self.after_mark_dispatching.set()
|
|
assert self.release_fake_publish.wait(3.0)
|
|
self.fake_client_publish_calls.append(envelope.payload_sha256)
|
|
raise RuntimeError("synthetic response omitted after fake client publish")
|
|
finally:
|
|
if release is not None:
|
|
release()
|
|
|
|
|
|
class _ActualWorkerStopRaceExecutor:
|
|
def __init__(self, transport: _ActualWorkerStopRaceTransport) -> None:
|
|
self.transport = transport
|
|
self.binding = LiveDeviceControlBinding(
|
|
vendor_device_id="actual-worker-device",
|
|
device_serial="ACTUAL-WORKER-SERIAL",
|
|
software_version="3.0.2",
|
|
system_version="3.0.2",
|
|
device_model="LixelKity K1",
|
|
device_type="A4",
|
|
is_activated=True,
|
|
)
|
|
self.stage = "inspection"
|
|
|
|
def run_read_only_inspection_stage(self, _orchestrator: object) -> LiveDeviceControlBinding:
|
|
observer = self.transport.evidence_observer
|
|
assert observer is not None
|
|
observer.application_response(
|
|
ApplicationMqttResponseEvidence(
|
|
operation_key="bootstrap:1:DeviceInfoRequest",
|
|
response_topic="lixel/application/response/device_info",
|
|
payload_sha256="8" * 64,
|
|
modeling_action=None,
|
|
result_code=None,
|
|
success=None,
|
|
observed_at_utc="2026-08-12T09:00:00.000Z",
|
|
)
|
|
)
|
|
return self.binding
|
|
|
|
def wait_for_operator_checkpoint(
|
|
self,
|
|
_event: str,
|
|
observed: Callable[[], bool],
|
|
*,
|
|
reconciled_active_observed: Callable[[], bool] | None = None,
|
|
) -> None:
|
|
deadline = time.monotonic() + 3.0
|
|
while not observed():
|
|
if reconciled_active_observed is not None and reconciled_active_observed():
|
|
return None
|
|
if time.monotonic() >= deadline:
|
|
raise TimeoutError("actual worker checkpoint was not released")
|
|
threading.Event().wait(0.005)
|
|
return None
|
|
|
|
def adopt_reconciled_scanning(self, **_: object) -> None:
|
|
self.stage = "scanning"
|
|
|
|
def maintain_active_until_stop_requested(self, observed: Callable[[], bool]) -> None:
|
|
while not observed():
|
|
threading.Event().wait(0.005)
|
|
self.stage = "stop-requested"
|
|
|
|
def execute_canonical_stop(
|
|
self,
|
|
command: object,
|
|
_permit: object,
|
|
*,
|
|
dispatch_guard: Callable[[], None] | None = None,
|
|
dispatch_admission_deadline_reached: Callable[[], bool] | None = None,
|
|
) -> object:
|
|
self.transport.before_transport_guard.set()
|
|
assert self.transport.release_transport_guard.wait(3.0)
|
|
if dispatch_guard is not None:
|
|
dispatch_guard()
|
|
assert dispatch_admission_deadline_reached is not None
|
|
assert dispatch_admission_deadline_reached() is False
|
|
envelope = OneShotPublishEnvelope.from_modeling_command(command) # type: ignore[arg-type]
|
|
return self.transport.exchange_batch_once(
|
|
[envelope],
|
|
required_response_operation_keys={"modeling:stop"},
|
|
)
|
|
|
|
def maintain_post_stop_until_standby(self) -> None:
|
|
raise RuntimeError("synthetic standby omitted")
|
|
|
|
def snapshot(self) -> dict[str, object]:
|
|
return {
|
|
"dialogue_stage": self.stage,
|
|
"start_attempted": True,
|
|
"start_complete": True,
|
|
"stop_attempted": self.transport.publish_attempts > 0,
|
|
"stop_complete": False,
|
|
"automatic_retry": False,
|
|
}
|
|
|
|
|
|
def _wait_control_state(
|
|
session: InteractiveApplicationControlSession,
|
|
expected: set[str],
|
|
) -> dict[str, object]:
|
|
deadline = time.monotonic() + 3.0
|
|
while time.monotonic() < deadline:
|
|
snapshot = session.snapshot()
|
|
if str(snapshot.get("state")) in expected:
|
|
return snapshot
|
|
threading.Event().wait(0.005)
|
|
raise AssertionError(f"control did not reach {expected}: {session.snapshot()}")
|
|
|
|
|
|
def _install_actual_worker_recovered_stop_fixture(
|
|
service: XgridsK1CompatibilityService,
|
|
runtime: FakeVisualizationRuntime,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> SimpleNamespace:
|
|
binding = _seed_supervised_connection(
|
|
service,
|
|
transport_ref="actual-worker-transport",
|
|
logical_device_id="actual-worker-device",
|
|
with_control=False,
|
|
)
|
|
route_available = {"value": True}
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_sample_host_path",
|
|
lambda _target, **_kwargs: (
|
|
_direct_host_path(binding.target_ipv4)
|
|
if route_available["value"]
|
|
else HostPathProbeResult(
|
|
available=False,
|
|
fingerprint=None,
|
|
interface=None,
|
|
source_ipv4=None,
|
|
route_class="unavailable",
|
|
reason_code="router-link-lost",
|
|
)
|
|
),
|
|
)
|
|
|
|
identity = PhysicalCommandIdentity(
|
|
vendor_device_id_sha256=hashlib.sha256(b"actual-worker-device").hexdigest(),
|
|
device_serial_sha256=hashlib.sha256(b"ACTUAL-WORKER-SERIAL").hexdigest(),
|
|
)
|
|
old_connection = PhysicalCommandConnectionBinding(
|
|
intent_id="old-active-intent",
|
|
transport_ref=binding.transport_ref,
|
|
connection_mode=binding.connection_mode,
|
|
target_ipv4=binding.target_ipv4,
|
|
target_port=binding.target_port,
|
|
host_path_epoch=binding.host_path_epoch,
|
|
control_session_id="old-active-control",
|
|
producer_generation=1,
|
|
)
|
|
|
|
def physical_status(state: str, observed: str) -> PhysicalCommandStatusEvidence:
|
|
scanning = state == "scanning"
|
|
return PhysicalCommandStatusEvidence(
|
|
source="live-control-session",
|
|
vendor_device_id_sha256=identity.vendor_device_id_sha256,
|
|
device_serial_sha256=identity.device_serial_sha256,
|
|
control_session_id=old_connection.control_session_id,
|
|
host_path_epoch=old_connection.host_path_epoch,
|
|
producer_generation=old_connection.producer_generation,
|
|
session_state=state, # type: ignore[arg-type]
|
|
session_state_code=302 if scanning else 300,
|
|
project_bound=scanning,
|
|
project_id_sha256="a" * 64 if scanning else None,
|
|
init_ready=scanning,
|
|
status_message_sha256=hashlib.sha256(observed.encode()).hexdigest(),
|
|
mqtt_retained=False,
|
|
observed_at_utc=observed,
|
|
)
|
|
|
|
ledger = service._physical_command_ledger # noqa: SLF001
|
|
start_operation_id = "op-00000000-0000-4000-8000-000000001420"
|
|
acquisition_id = "acq-actual-worker-stop-race"
|
|
ledger.prepare(
|
|
operation_id=start_operation_id,
|
|
parent_operation_id=None,
|
|
acquisition_id=acquisition_id,
|
|
action="start",
|
|
identity=identity,
|
|
connection=old_connection,
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
payload_sha256="1" * 64,
|
|
baseline_status=physical_status("ready", "2026-08-12T09:00:01.000Z"),
|
|
)
|
|
ledger.mark_dispatching(start_operation_id)
|
|
ledger.mark_observing(start_operation_id, publish_call_returned=True, packet_id=81)
|
|
ledger.mark_qos2_completed(start_operation_id, packet_id=81)
|
|
ledger.record_application_response(
|
|
start_operation_id,
|
|
PhysicalCommandApplicationResponse(
|
|
operation_id=start_operation_id,
|
|
action="start",
|
|
control_session_id=old_connection.control_session_id,
|
|
host_path_epoch=old_connection.host_path_epoch,
|
|
producer_generation=old_connection.producer_generation,
|
|
result_code=PHYSICAL_COMMAND_APPLICATION_SUCCESS_CODE,
|
|
success=True,
|
|
payload_sha256="2" * 64,
|
|
observed_at_utc="2026-08-12T09:00:02.000Z",
|
|
),
|
|
)
|
|
ledger.record_status_observation(
|
|
start_operation_id,
|
|
physical_status("scanning", "2026-08-12T09:00:03.000Z"),
|
|
)
|
|
ledger.resolve(start_operation_id, resolution="start-active-observed")
|
|
|
|
transport = _ActualWorkerStopRaceTransport()
|
|
monkeypatch.setattr(
|
|
session_module,
|
|
"PhysicalAcceptanceDialogueExecutor",
|
|
_ActualWorkerStopRaceExecutor,
|
|
)
|
|
coordinator = service._physical_command_coordinator # noqa: SLF001
|
|
control = InteractiveApplicationControlSession(
|
|
FakeApplicationAuthorityLoader(),
|
|
transport_factory=lambda _host: transport, # type: ignore[arg-type]
|
|
connection_path_validator=service._validate_application_connection_path, # noqa: SLF001
|
|
connection_binding_validator=service._validate_application_connection_binding, # noqa: SLF001
|
|
connection_dispatch_lease=service._acquire_application_dispatch_lease, # noqa: SLF001
|
|
physical_command_coordinator=coordinator,
|
|
)
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
control.open(
|
|
host=binding.target_ipv4,
|
|
timezone_name="Europe/Moscow",
|
|
connection_binding=binding,
|
|
inspection_only=True,
|
|
)
|
|
ready = _wait_control_state(control, {"connection-ready"})
|
|
verified = ready["verified_control"]
|
|
assert isinstance(verified, dict)
|
|
coordinator.device_status(
|
|
ApplicationMqttDeviceStatusEvidence(
|
|
vendor_device_id_sha256=identity.vendor_device_id_sha256,
|
|
device_serial_sha256=identity.device_serial_sha256,
|
|
session_state="scanning",
|
|
session_state_code=MODELING_STATE_BASE + 302,
|
|
project_bound=True,
|
|
project_id_sha256="a" * 64,
|
|
init_ready=True,
|
|
status_message_sha256="3" * 64,
|
|
mqtt_retained=False,
|
|
observed_at_utc="2026-08-12T09:00:04.000Z",
|
|
)
|
|
)
|
|
reconciliation_id = "actual-worker-rebind"
|
|
reconciled = coordinator.reconcile_resolved_active(reconciliation_id=reconciliation_id)
|
|
control.adopt_reconciled_scanning(
|
|
reconciliation_id=reconciliation_id,
|
|
expected_session_generation=ready["session_generation"], # type: ignore[arg-type]
|
|
expected_state_revision=ready["state_revision"], # type: ignore[arg-type]
|
|
)
|
|
scanning = _wait_control_state(control, {"scanning"})
|
|
service._reconcile_connection_supervisor(scanning, runtime.snapshot()) # noqa: SLF001
|
|
service._materialize_recovered_physical_stop_acquisition( # noqa: SLF001
|
|
reconciliation_id=reconciliation_id,
|
|
reconciled_record=reconciled,
|
|
)
|
|
stop_operation_id = "op-00000000-0000-4000-8000-000000001421"
|
|
request = _stop_request(
|
|
acquisition_id=acquisition_id,
|
|
operation_id=stop_operation_id,
|
|
idempotency_key="actual-worker-stop-race",
|
|
mode="graceful",
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
expected_control_session_generation=scanning["session_generation"],
|
|
expected_control_state_revision=scanning["state_revision"],
|
|
)
|
|
return SimpleNamespace(
|
|
binding=binding,
|
|
route_available=route_available,
|
|
ledger=ledger,
|
|
coordinator=coordinator,
|
|
control=control,
|
|
transport=transport,
|
|
request=request,
|
|
stop_operation_id=stop_operation_id,
|
|
)
|
|
|
|
|
|
def test_capture_only_rejects_real_worker_queued_before_prepared_dispatch(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_actual_worker_recovered_stop_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
)
|
|
service.stop_acquisition(fixture.request)
|
|
assert fixture.transport.before_transport_guard.wait(3.0)
|
|
before_operations = tuple(service._operations.snapshot()) # noqa: SLF001
|
|
before_acquisition = service._acquisition # noqa: SLF001
|
|
|
|
with pytest.raises(
|
|
facade_module.LocalAcquisitionLifecycleError,
|
|
match="ещё владеет PREPARED STOP",
|
|
) as raised:
|
|
service.stop_acquisition(
|
|
_stop_request(
|
|
acquisition_id=fixture.request.acquisition_id,
|
|
operation_id="op-00000000-0000-4000-8000-000000001458",
|
|
mode="capture-only",
|
|
)
|
|
)
|
|
|
|
assert raised.value.reason_code == "acquisition-stop-worker-retirement-pending"
|
|
assert tuple(service._operations.snapshot()) == before_operations # noqa: SLF001
|
|
assert service._acquisition is before_acquisition # noqa: SLF001
|
|
assert fixture.transport.fake_client_publish_calls == []
|
|
assert fixture.transport.publish_attempts == 0
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None and record.stage == "prepared"
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.status == "running"
|
|
assert operation.error is None
|
|
assert runtime.stop_calls == 0
|
|
|
|
# Rejection performs no socket close or cancellation. The one accepted
|
|
# canonical S0 worker remains the sole owner and may cross its original
|
|
# publish edge exactly once after the test releases it.
|
|
fixture.transport.release_transport_guard.set()
|
|
assert fixture.transport.after_mark_dispatching.wait(3.0)
|
|
fixture.transport.release_fake_publish.set()
|
|
_wait_control_state(fixture.control, {"failed"})
|
|
assert len(fixture.transport.fake_client_publish_calls) == 1
|
|
assert fixture.transport.publish_attempts == 1
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None and record.stage in {"dispatching", "observing"}
|
|
|
|
|
|
def test_actual_worker_stop_survives_both_loss_reducer_seams_and_publishes_once(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_actual_worker_recovered_stop_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
)
|
|
service.stop_acquisition(fixture.request)
|
|
assert fixture.transport.before_transport_guard.wait(3.0)
|
|
|
|
supervisor = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
assert supervisor.intent is not None and supervisor.device_network.target is not None
|
|
for _ in range(3):
|
|
assert service._connection_supervisor.observe_endpoint( # noqa: SLF001
|
|
target=supervisor.device_network.target,
|
|
intent_id=supervisor.intent.intent_id,
|
|
host_path_epoch=supervisor.host_path.epoch,
|
|
reachable=False,
|
|
reason_code="transient-control-port-negative",
|
|
)
|
|
retained = service.state()
|
|
assert retained["selected_device_id"] == fixture.binding.transport_ref
|
|
assert fixture.transport.fake_client_publish_calls == []
|
|
|
|
# Restore the exact control-plane proof after the synthetic transient
|
|
# endpoint negatives; the first seam tested only PREPARED lineage.
|
|
service._reconcile_connection_supervisor( # noqa: SLF001
|
|
fixture.control.snapshot(),
|
|
runtime.snapshot(),
|
|
)
|
|
refreshed = fixture.control.snapshot()
|
|
refreshed_verified = refreshed.get("verified_control")
|
|
assert isinstance(refreshed_verified, dict)
|
|
supervisor = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
assert supervisor.intent is not None and supervisor.device_network.target is not None
|
|
assert service._connection_supervisor.observe_endpoint( # noqa: SLF001
|
|
target=supervisor.device_network.target,
|
|
intent_id=supervisor.intent.intent_id,
|
|
host_path_epoch=supervisor.host_path.epoch,
|
|
reachable=True,
|
|
)
|
|
assert service._connection_supervisor.observe_control_evidence( # noqa: SLF001
|
|
VerifiedControlEvidence(
|
|
intent_id=supervisor.intent.intent_id,
|
|
transport_ref=fixture.binding.transport_ref,
|
|
host_path_epoch=supervisor.host_path.epoch,
|
|
target=supervisor.device_network.target,
|
|
connection_mode=fixture.binding.connection_mode,
|
|
logical_device_id="actual-worker-device",
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
control_session_id=str(refreshed_verified["control_session_id"]),
|
|
control_proof_revision=2,
|
|
control_proof_source="correlated-application-response",
|
|
)
|
|
)
|
|
fixture.transport.release_transport_guard.set()
|
|
assert fixture.transport.after_mark_dispatching.wait(3.0)
|
|
|
|
# The real worker now owns the facade dispatch gate after mark_dispatching.
|
|
# A true route loss cannot tear topology out from under fake client.publish.
|
|
for _ in range(3):
|
|
_observe_stop_race_route_loss(service)
|
|
mid_publish = service.state()
|
|
assert mid_publish["selected_device_id"] == fixture.binding.transport_ref
|
|
assert fixture.transport.fake_client_publish_calls == []
|
|
fixture.transport.release_fake_publish.set()
|
|
failed = _wait_control_state(fixture.control, {"failed"})
|
|
assert failed["state"] == "failed"
|
|
assert len(fixture.transport.fake_client_publish_calls) == 1
|
|
assert fixture.transport.publish_attempts == 1
|
|
after_failure = service.state()
|
|
physical_record = fixture.ledger.snapshot().record
|
|
assert physical_record is not None
|
|
assert physical_record.stage in {"dispatching", "observing"}
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.stage_code != "physical-stop-classified-not-dispatched"
|
|
assert operation.error is None or operation.error.get("side_effect_status") != "none"
|
|
assert after_failure["physical_command"]["record"]["stage"] in {
|
|
"dispatching",
|
|
"observing",
|
|
}
|
|
|
|
|
|
def test_actual_worker_real_route_loss_fails_before_publish_and_settles_none(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_actual_worker_recovered_stop_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
)
|
|
service.stop_acquisition(fixture.request)
|
|
assert fixture.transport.before_transport_guard.wait(3.0)
|
|
fixture.route_available["value"] = False
|
|
for _ in range(3):
|
|
_observe_stop_race_route_loss(service)
|
|
retained = service.state()
|
|
assert retained["selected_device_id"] == fixture.binding.transport_ref
|
|
|
|
fixture.transport.release_transport_guard.set()
|
|
failed = _wait_control_state(fixture.control, {"failed"})
|
|
assert failed["failure"]["reason_code"] == "application-connection-binding-lost" # type: ignore[index]
|
|
assert fixture.transport.fake_client_publish_calls == []
|
|
assert fixture.transport.publish_attempts == 0
|
|
settled = service.state()
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.status == "failed"
|
|
assert operation.stage_code == "physical-stop-classified-not-dispatched"
|
|
assert operation.error is not None
|
|
assert operation.error["side_effect_status"] == "none"
|
|
assert operation.error["physical_command_sent"] is False
|
|
assert settled["selected_device_id"] is None
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("durable_start_resolved", "expected_side_effect_status"),
|
|
[(False, "unknown"), (True, "succeeded")],
|
|
)
|
|
@pytest.mark.parametrize("coincident_local_error", ["none", "camera", "runtime"])
|
|
def test_plugin_start_requires_composite_active_proof_and_fails_closed_on_control_fault(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
durable_start_resolved: bool,
|
|
expected_side_effect_status: str,
|
|
coincident_local_error: str,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control = FakeInteractiveControlSession()
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
binding = _seed_supervised_connection(service)
|
|
service._compatibility_attestation = ATTESTATION.model_dump(mode="json") # noqa: SLF001
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="START_GATE",
|
|
host=binding.target_ipv4,
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
pre_pcl_state = service.start_acquisition(
|
|
_start_request(
|
|
acquisition_id=acquisition_id,
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
)
|
|
pre_pcl_camera_streams = [
|
|
stream
|
|
for stream in pre_pcl_state["sensor_catalog"]["streams"]
|
|
if stream.get("semantic_channel_id") == "camera.preview.live"
|
|
]
|
|
assert pre_pcl_state["camera_preview"]["active_source_id"] is None
|
|
assert pre_pcl_state["camera_preview"]["delivery"] is None
|
|
assert pre_pcl_state["camera_preview"]["activation_admission"]["state"] == (
|
|
"waiting-for-first-authoritative-pcl"
|
|
)
|
|
assert all(stream["activation"]["selected"] is False for stream in pre_pcl_camera_streams)
|
|
assert all(stream["activation"]["controllable"] is False for stream in pre_pcl_camera_streams)
|
|
start_operation_id = service._acquisition_start_operation_id # noqa: SLF001
|
|
assert isinstance(start_operation_id, str)
|
|
physical_proof = _exact_start_physical_proof(
|
|
operation_id=start_operation_id,
|
|
acquisition_id=acquisition_id,
|
|
binding=binding,
|
|
resolved=durable_start_resolved,
|
|
)
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"snapshot",
|
|
lambda: physical_proof,
|
|
)
|
|
|
|
# The live stream can begin during SCAN_STARTING. Neither frames alone nor
|
|
# even a durable edge observed after this browser reduction is enough while
|
|
# the current canonical control owner is not SCANNING/can_stop.
|
|
control.state = "initializing"
|
|
runtime.mark_ready()
|
|
runtime.pcl_frames = 30
|
|
initializing = service.state()
|
|
assert initializing["acquisition"]["state"] == "awaiting_external_start"
|
|
assert initializing["last_operation"]["status"] == "running"
|
|
|
|
original_control_snapshot = control.snapshot
|
|
|
|
def failed_control_snapshot() -> dict[str, object]:
|
|
snapshot = original_control_snapshot()
|
|
snapshot["outcome_unknown"] = True
|
|
snapshot["failure"] = {
|
|
"code": "ApplicationControlDeviceFault",
|
|
"reason_code": "device_status_fault",
|
|
"failed_phase": "initializing",
|
|
"modeling_command_attempted": True,
|
|
"safe_to_retry": False,
|
|
}
|
|
snapshot["transport"] = {
|
|
"latest_system_error_code": 0x32040133,
|
|
"latest_system_error_state": "algorithm_error",
|
|
}
|
|
return snapshot
|
|
|
|
if coincident_local_error == "camera":
|
|
original_camera_snapshot = service.camera_preview.snapshot
|
|
|
|
def failed_camera_snapshot() -> dict[str, Any]:
|
|
return {
|
|
**original_camera_snapshot(),
|
|
"phase": "error",
|
|
"recording": {"active": True},
|
|
"error": {"code": "coincident-camera-fault"},
|
|
}
|
|
|
|
monkeypatch.setattr(service.camera_preview, "snapshot", failed_camera_snapshot)
|
|
elif coincident_local_error == "runtime":
|
|
runtime.phase = "error"
|
|
|
|
monkeypatch.setattr(control, "snapshot", failed_control_snapshot)
|
|
control.state = "failed"
|
|
failed = service.state()
|
|
start_operation = next(
|
|
operation
|
|
for operation in failed["operations"]
|
|
if operation["operation_id"] == start_operation_id
|
|
)
|
|
assert failed["acquisition"]["state"] == "failed"
|
|
assert failed["acquisition"]["cleanup_pending"] is True
|
|
assert failed["source_mode"] == "live"
|
|
assert start_operation["status"] == "failed"
|
|
assert start_operation["stage_code"] == "device-start-outcome-unknown"
|
|
assert start_operation["error"]["safe_to_retry"] is False
|
|
assert start_operation["error"]["side_effect_status"] == (expected_side_effect_status)
|
|
assert start_operation["error"]["automatic_replay_allowed"] is False
|
|
assert runtime.stop_calls == 0
|
|
assert control.stop_calls == 0
|
|
|
|
runtime.pcl_frames += 1
|
|
still_failed = service.state()
|
|
repeated_start_operation = next(
|
|
operation
|
|
for operation in still_failed["operations"]
|
|
if operation["operation_id"] == start_operation_id
|
|
)
|
|
assert still_failed["acquisition"]["state"] == "failed"
|
|
assert still_failed["acquisition"]["cleanup_pending"] is True
|
|
assert still_failed["source_mode"] == "live"
|
|
assert repeated_start_operation == start_operation
|
|
assert runtime.stop_calls == 0
|
|
assert control.stop_calls == 0
|
|
|
|
locally_stopped = service.stop_acquisition(
|
|
_stop_request(acquisition_id=acquisition_id, mode="capture-only")
|
|
)
|
|
assert locally_stopped["source_mode"] == "idle"
|
|
assert locally_stopped["acquisition"]["cleanup_pending"] is False
|
|
assert locally_stopped["last_operation"]["result"]["device_stop"] == "unknown"
|
|
assert runtime.stop_calls == 1
|
|
assert control.stop_calls == 0
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("durable_start_resolved", "expected_side_effect_status"),
|
|
[(False, "unknown"), (True, "succeeded")],
|
|
)
|
|
@pytest.mark.parametrize("local_error", ["camera", "runtime"])
|
|
def test_plugin_start_local_failure_preserves_post_checkpoint_physical_outcome(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
durable_start_resolved: bool,
|
|
expected_side_effect_status: str,
|
|
local_error: str,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control = FakeInteractiveControlSession()
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
binding = _seed_supervised_connection(service)
|
|
service._compatibility_attestation = ATTESTATION.model_dump(mode="json") # noqa: SLF001
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="START_LOCAL_FAILURE",
|
|
host=binding.target_ipv4,
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
pre_pcl_state = service.start_acquisition(
|
|
_start_request(
|
|
acquisition_id=acquisition_id,
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
)
|
|
pre_pcl_camera_streams = [
|
|
stream
|
|
for stream in pre_pcl_state["sensor_catalog"]["streams"]
|
|
if stream.get("semantic_channel_id") == "camera.preview.live"
|
|
]
|
|
assert pre_pcl_state["camera_preview"]["active_source_id"] is None
|
|
assert pre_pcl_state["camera_preview"]["delivery"] is None
|
|
assert pre_pcl_state["camera_preview"]["activation_admission"]["state"] == (
|
|
"waiting-for-first-authoritative-pcl"
|
|
)
|
|
assert all(stream["activation"]["selected"] is False for stream in pre_pcl_camera_streams)
|
|
assert all(stream["activation"]["controllable"] is False for stream in pre_pcl_camera_streams)
|
|
start_operation_id = service._acquisition_start_operation_id # noqa: SLF001
|
|
assert isinstance(start_operation_id, str)
|
|
physical_proof = _exact_start_physical_proof(
|
|
operation_id=start_operation_id,
|
|
acquisition_id=acquisition_id,
|
|
binding=binding,
|
|
resolved=durable_start_resolved,
|
|
)
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"snapshot",
|
|
lambda: physical_proof,
|
|
)
|
|
runtime.mark_ready()
|
|
awaiting = service.state()
|
|
assert awaiting["acquisition"]["state"] == "awaiting_external_start"
|
|
assert awaiting["last_operation"]["status"] == "running"
|
|
|
|
if local_error == "camera":
|
|
original_camera_snapshot = service.camera_preview.snapshot
|
|
|
|
def failed_camera_snapshot() -> dict[str, Any]:
|
|
return {
|
|
**original_camera_snapshot(),
|
|
"phase": "error",
|
|
"recording": {"active": True},
|
|
"error": {"code": "camera-failed-before-local-start-acceptance"},
|
|
}
|
|
|
|
monkeypatch.setattr(service.camera_preview, "snapshot", failed_camera_snapshot)
|
|
else:
|
|
runtime.phase = "error"
|
|
|
|
failed = service.state()
|
|
start_operation = next(
|
|
operation
|
|
for operation in failed["operations"]
|
|
if operation["operation_id"] == start_operation_id
|
|
)
|
|
assert failed["acquisition"]["state"] == "failed"
|
|
assert failed["acquisition"]["cleanup_pending"] is False
|
|
assert failed["source_mode"] == "idle"
|
|
assert start_operation["status"] == "failed"
|
|
assert start_operation["stage_code"] == "runtime-failed"
|
|
assert start_operation["error"]["safe_to_retry"] is False
|
|
assert start_operation["error"]["side_effect_status"] == (expected_side_effect_status)
|
|
assert start_operation["error"]["automatic_replay_allowed"] is False
|
|
assert runtime.stop_calls == 1
|
|
assert control.stop_calls == 0
|
|
|
|
|
|
@pytest.mark.parametrize("local_failure", ["none", "cleanup", "camera"])
|
|
def test_durable_stop_ready_auto_cleans_local_receiver_after_control_loss(
|
|
tmp_path: Path,
|
|
local_failure: str,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""A resolved exact STOP survives Wi-Fi/control retirement as completion truth."""
|
|
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control = FakeInteractiveControlSession()
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
binding = _seed_supervised_connection(
|
|
service,
|
|
connection_mode="bridge",
|
|
transport_ref="k1-stop-ready",
|
|
)
|
|
control.verified_control = _verified_control_for_binding(
|
|
binding,
|
|
control_session_id="stop-ready-control",
|
|
)
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="STOP_READY",
|
|
host=binding.target_ipv4,
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(
|
|
_start_request(
|
|
acquisition_id=acquisition_id,
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
)
|
|
start_operation_id = service._acquisition_start_operation_id # noqa: SLF001
|
|
assert isinstance(start_operation_id, str)
|
|
|
|
identity = PhysicalCommandIdentity(
|
|
vendor_device_id_sha256="a" * 64,
|
|
device_serial_sha256="b" * 64,
|
|
)
|
|
physical_connection = PhysicalCommandConnectionBinding(
|
|
intent_id=binding.intent_id,
|
|
transport_ref=binding.transport_ref,
|
|
connection_mode="bridge",
|
|
target_ipv4=binding.target_ipv4,
|
|
target_port=binding.target_port,
|
|
host_path_epoch=binding.host_path_epoch,
|
|
control_session_id="stop-ready-control",
|
|
producer_generation=1,
|
|
)
|
|
|
|
def status(
|
|
session_state: str,
|
|
*,
|
|
observed_at_utc: str,
|
|
) -> PhysicalCommandStatusEvidence:
|
|
scanning = session_state == "scanning"
|
|
return PhysicalCommandStatusEvidence(
|
|
source="live-control-session",
|
|
vendor_device_id_sha256=identity.vendor_device_id_sha256,
|
|
device_serial_sha256=identity.device_serial_sha256,
|
|
control_session_id=physical_connection.control_session_id,
|
|
host_path_epoch=physical_connection.host_path_epoch,
|
|
producer_generation=physical_connection.producer_generation,
|
|
session_state=session_state, # type: ignore[arg-type]
|
|
session_state_code=302 if scanning else 300,
|
|
project_bound=scanning,
|
|
project_id_sha256="c" * 64 if scanning else None,
|
|
init_ready=scanning,
|
|
status_message_sha256=hashlib.sha256(
|
|
f"{session_state}:{observed_at_utc}".encode()
|
|
).hexdigest(),
|
|
mqtt_retained=False,
|
|
observed_at_utc=observed_at_utc,
|
|
)
|
|
|
|
ledger = service._physical_command_ledger # noqa: SLF001
|
|
ledger.prepare(
|
|
operation_id=start_operation_id,
|
|
parent_operation_id=None,
|
|
acquisition_id=acquisition_id,
|
|
action="start",
|
|
identity=identity,
|
|
connection=physical_connection,
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
payload_sha256="1" * 64,
|
|
baseline_status=status(
|
|
"ready",
|
|
observed_at_utc="2026-08-10T08:01:30.000Z",
|
|
),
|
|
)
|
|
ledger.mark_dispatching(start_operation_id)
|
|
ledger.mark_observing(
|
|
start_operation_id,
|
|
publish_call_returned=True,
|
|
packet_id=94,
|
|
)
|
|
ledger.mark_qos2_completed(start_operation_id, packet_id=94)
|
|
ledger.record_application_response(
|
|
start_operation_id,
|
|
PhysicalCommandApplicationResponse(
|
|
operation_id=start_operation_id,
|
|
action="start",
|
|
control_session_id=physical_connection.control_session_id,
|
|
host_path_epoch=physical_connection.host_path_epoch,
|
|
producer_generation=physical_connection.producer_generation,
|
|
result_code=PHYSICAL_COMMAND_APPLICATION_SUCCESS_CODE,
|
|
success=True,
|
|
payload_sha256="2" * 64,
|
|
observed_at_utc="2026-08-10T08:01:31.000Z",
|
|
),
|
|
)
|
|
ledger.record_status_observation(
|
|
start_operation_id,
|
|
status("scanning", observed_at_utc="2026-08-10T08:01:32.000Z"),
|
|
)
|
|
ledger.resolve(start_operation_id, resolution="start-active-observed")
|
|
runtime.mark_ready()
|
|
runtime.pcl_frames = 1
|
|
assert service.state()["acquisition"]["state"] == "acquiring"
|
|
|
|
# Match an acquisition that resumed through the exact read-only recovery
|
|
# owner before the operator requested the one canonical STOP.
|
|
active_start_record = ledger.snapshot().record
|
|
assert active_start_record is not None
|
|
recovery_lineage = facade_module._ActiveStreamRecoveryLineage( # noqa: SLF001
|
|
recovery_generation=service._active_stream_recovery_generation, # noqa: SLF001
|
|
snapshot_runtime_id=service._snapshot_runtime_id, # noqa: SLF001
|
|
acquisition_id=acquisition_id,
|
|
device_id=service._device_id, # type: ignore[arg-type] # noqa: SLF001
|
|
device_session_id=service._device_session_id, # type: ignore[arg-type] # noqa: SLF001
|
|
evidence_session_id=service._acquisition_out_dir.name, # type: ignore[union-attr] # noqa: SLF001
|
|
runtime_producer_generation=runtime.producer_generation,
|
|
start_operation_id=start_operation_id,
|
|
physical_operation_id=start_operation_id,
|
|
physical_revision=active_start_record.revision,
|
|
intent_id=binding.intent_id,
|
|
transport_ref=binding.transport_ref,
|
|
connection_mode="bridge",
|
|
target_ipv4=binding.target_ipv4,
|
|
target_port=binding.target_port,
|
|
)
|
|
with service._lock: # noqa: SLF001
|
|
service._active_stream_recovery_lineage = recovery_lineage # noqa: SLF001
|
|
service._active_stream_recovery_state = "recovered" # noqa: SLF001
|
|
service._active_stream_recovery_attempt = 7 # noqa: SLF001
|
|
runtime.recovery_state = "recovered"
|
|
runtime.recovery_attempt = 7
|
|
|
|
stop_operation_id = "op-00000000-0000-4000-8000-000000001299"
|
|
stop_request = _stop_request(
|
|
acquisition_id=acquisition_id,
|
|
operation_id=stop_operation_id,
|
|
mode="graceful",
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
stopping = service.stop_acquisition(stop_request)
|
|
assert stopping["acquisition"]["state"] == "awaiting_external_stop"
|
|
assert stopping["last_operation"]["status"] == "running"
|
|
assert control.stop_calls == 1
|
|
assert runtime.source_mode == "live"
|
|
|
|
# Wi-Fi/control disappears after the one STOP request. Model the exact
|
|
# production ordering: state() begins with a pending local retirement,
|
|
# turns the completed ephemeral control owner into idle, and only then
|
|
# reduces the durable READY edge.
|
|
control.state = "completed"
|
|
service._pending_local_control_retirement = True # noqa: SLF001
|
|
ledger.prepare(
|
|
operation_id=stop_operation_id,
|
|
parent_operation_id=start_operation_id,
|
|
acquisition_id=acquisition_id,
|
|
action="stop",
|
|
identity=identity,
|
|
connection=physical_connection,
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
payload_sha256="d" * 64,
|
|
baseline_status=status(
|
|
"scanning",
|
|
observed_at_utc="2026-08-10T08:01:40.000Z",
|
|
),
|
|
)
|
|
ledger.mark_dispatching(stop_operation_id)
|
|
ledger.mark_observing(
|
|
stop_operation_id,
|
|
publish_call_returned=True,
|
|
packet_id=95,
|
|
)
|
|
ledger.mark_qos2_completed(stop_operation_id, packet_id=95)
|
|
ledger.record_application_response(
|
|
stop_operation_id,
|
|
PhysicalCommandApplicationResponse(
|
|
operation_id=stop_operation_id,
|
|
action="stop",
|
|
control_session_id=physical_connection.control_session_id,
|
|
host_path_epoch=physical_connection.host_path_epoch,
|
|
producer_generation=physical_connection.producer_generation,
|
|
result_code=PHYSICAL_COMMAND_APPLICATION_SUCCESS_CODE,
|
|
success=True,
|
|
payload_sha256="e" * 64,
|
|
observed_at_utc="2026-08-10T08:01:41.000Z",
|
|
),
|
|
)
|
|
ledger.record_status_observation(
|
|
stop_operation_id,
|
|
status("ready", observed_at_utc="2026-08-10T08:01:44.000Z"),
|
|
)
|
|
ledger.resolve(stop_operation_id, resolution="stop-standby-observed")
|
|
|
|
unintended_edges: list[str] = []
|
|
|
|
async def forbidden_device_edge(*_args: object, **_kwargs: object) -> object:
|
|
unintended_edges.append("device-or-network-io")
|
|
raise AssertionError("durable STOP READY reduction must be local-only")
|
|
|
|
monkeypatch.setattr(facade_module, "scan", forbidden_device_edge)
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_device_edge)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_device_edge)
|
|
|
|
if local_failure == "cleanup":
|
|
runtime.stop_error = RuntimeError("synthetic local cleanup failure after READY")
|
|
elif local_failure == "camera":
|
|
original_camera_snapshot = service.camera_preview.snapshot
|
|
|
|
def failed_camera_snapshot() -> dict[str, Any]:
|
|
snapshot = original_camera_snapshot()
|
|
return {
|
|
**snapshot,
|
|
"phase": "error",
|
|
"recording": {"active": True},
|
|
"error": {"code": "synthetic-camera-producer-failed"},
|
|
}
|
|
|
|
monkeypatch.setattr(service.camera_preview, "snapshot", failed_camera_snapshot)
|
|
|
|
completed = service.state()
|
|
operation = next(
|
|
item for item in completed["operations"] if item["operation_id"] == stop_operation_id
|
|
)
|
|
|
|
assert completed["application_control_session"]["state"] == "idle"
|
|
assert completed["physical_command"]["record"]["resolution"] == ("stop-standby-observed")
|
|
if local_failure == "cleanup":
|
|
assert completed["acquisition"]["state"] == "failed"
|
|
assert completed["acquisition"]["cleanup_pending"] is True
|
|
assert operation["status"] == "failed"
|
|
assert operation["stage_code"] == "local-finalization-failed"
|
|
assert operation["error"] == {
|
|
"category": "stream",
|
|
"code": "local-finalization-failed-after-device-standby",
|
|
"retryable": True,
|
|
"safe_to_retry": True,
|
|
"side_effect_status": "succeeded",
|
|
}
|
|
assert completed["source_mode"] == "live"
|
|
assert completed["connection_policy"]["actions"]["scan-ble"]["allowed"] is False
|
|
|
|
# Local cleanup alone is retried by polling. The physical STOP remains
|
|
# terminal and is never published again.
|
|
runtime.stop_error = None
|
|
completed = service.state()
|
|
operation = next(
|
|
item for item in completed["operations"] if item["operation_id"] == stop_operation_id
|
|
)
|
|
assert completed["acquisition"]["state"] == "failed"
|
|
assert completed["acquisition"]["cleanup_pending"] is False
|
|
assert operation["status"] == "failed"
|
|
assert operation["stage_code"] == "local-finalization-failed"
|
|
assert runtime.stop_calls == 2
|
|
elif local_failure == "camera":
|
|
assert completed["acquisition"]["state"] == "failed"
|
|
assert completed["acquisition"]["cleanup_pending"] is False
|
|
assert completed["acquisition"]["result"] == {
|
|
"receiver_stopped": True,
|
|
"device_state": "ready",
|
|
"device_stop": "protocol-confirmed",
|
|
"camera_failure_code": "synthetic-camera-producer-failed",
|
|
}
|
|
assert operation["status"] == "failed"
|
|
assert operation["stage_code"] == "local-finalization-failed"
|
|
assert operation["error"]["side_effect_status"] == "succeeded"
|
|
assert runtime.stop_calls == 1
|
|
else:
|
|
assert completed["acquisition"]["state"] == "completed"
|
|
assert completed["acquisition"]["cleanup_pending"] is False
|
|
assert completed["acquisition"]["result"] == {
|
|
"receiver_stopped": True,
|
|
"device_state": "ready",
|
|
"device_stop": "protocol-confirmed",
|
|
}
|
|
assert operation["status"] == "succeeded"
|
|
assert operation["stage_code"] == "device-standby-confirmed"
|
|
assert runtime.stop_calls == 1
|
|
assert completed["source_mode"] == "idle"
|
|
assert completed["live_perception_shadow"]["active"] is False
|
|
assert completed["connection_policy"]["actions"]["scan-ble"]["allowed"] is True
|
|
assert unintended_edges == []
|
|
assert control.stop_calls == 1
|
|
if local_failure != "cleanup":
|
|
assert completed["connection_recovery"]["state"] == "inactive"
|
|
assert completed["connection_recovery"]["automatic_read_only_rebind"] is False
|
|
assert service._active_stream_recovery_lineage is None # noqa: SLF001
|
|
|
|
# Polling the same durable proof is a pure idempotent read: no second
|
|
# local teardown and, critically, no second physical STOP.
|
|
stop_call_count = runtime.stop_calls
|
|
repeated = service.state()
|
|
repeated_operation = next(
|
|
item for item in repeated["operations"] if item["operation_id"] == stop_operation_id
|
|
)
|
|
assert repeated_operation["status"] == operation["status"]
|
|
assert repeated["acquisition"]["state"] == completed["acquisition"]["state"]
|
|
assert repeated["connection_policy"]["actions"]["scan-ble"]["allowed"] is True
|
|
assert unintended_edges == []
|
|
assert control.stop_calls == 1
|
|
assert runtime.stop_calls == stop_call_count
|
|
|
|
# A process restart retains only the resolved durable edge. There is no
|
|
# process-local receiver to recover and no physical action to replay.
|
|
restarted, restarted_runtime = service_with_fake_runtime(tmp_path)
|
|
restarted_state = restarted.state()
|
|
assert restarted_state["physical_command"]["record"]["operation_id"] == (stop_operation_id)
|
|
assert restarted_state["physical_command"]["record"]["resolution"] == ("stop-standby-observed")
|
|
assert restarted_state["acquisition"] is None
|
|
assert restarted_state["source_mode"] == "idle"
|
|
assert restarted_state["connection_policy"]["actions"]["scan-ble"]["allowed"] is True
|
|
assert restarted_runtime.stop_calls == 0
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mismatch",
|
|
[
|
|
"operation",
|
|
"acquisition",
|
|
"response-operation",
|
|
"retained-ready",
|
|
"bound-ready",
|
|
"initialized-ready",
|
|
"missing-qos2",
|
|
],
|
|
)
|
|
def test_durable_stop_ready_rejects_nonmatching_or_noncanonical_proof(
|
|
mismatch: str,
|
|
) -> None:
|
|
stop_operation_id = "op-00000000-0000-4000-8000-000000001399"
|
|
acquisition_id = "acquisition-stop-ready-proof"
|
|
proof: dict[str, Any] = {
|
|
"status": "resolved",
|
|
"requires_reconciliation": False,
|
|
"record": {
|
|
"operation_id": stop_operation_id,
|
|
"acquisition_id": acquisition_id,
|
|
"action": "stop",
|
|
"stage": "resolved",
|
|
"resolution": "stop-standby-observed",
|
|
"publish_call_returned": True,
|
|
"qos2_completed": True,
|
|
"packet_id": 91,
|
|
"application_response": {
|
|
"operation_id": stop_operation_id,
|
|
"action": "stop",
|
|
"success": True,
|
|
},
|
|
"last_status": {
|
|
"session_state": "ready",
|
|
"project_bound": False,
|
|
"init_ready": False,
|
|
"mqtt_retained": False,
|
|
},
|
|
},
|
|
}
|
|
record = proof["record"]
|
|
if mismatch == "operation":
|
|
record["operation_id"] = "different-stop-operation"
|
|
elif mismatch == "acquisition":
|
|
record["acquisition_id"] = "different-acquisition"
|
|
elif mismatch == "response-operation":
|
|
record["application_response"]["operation_id"] = "different-stop-operation"
|
|
elif mismatch == "retained-ready":
|
|
record["last_status"]["mqtt_retained"] = True
|
|
elif mismatch == "bound-ready":
|
|
record["last_status"]["project_bound"] = True
|
|
elif mismatch == "initialized-ready":
|
|
record["last_status"]["init_ready"] = True
|
|
elif mismatch == "missing-qos2":
|
|
record["qos2_completed"] = False
|
|
|
|
assert (
|
|
XgridsK1CompatibilityService._matching_stop_standby_confirmed( # noqa: SLF001
|
|
proof,
|
|
acquisition_id=acquisition_id,
|
|
stop_operation_id=stop_operation_id,
|
|
)
|
|
is False
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mismatch",
|
|
["operation", "acquisition", "response-operation", "response-failed", "ready", "stopping"],
|
|
)
|
|
def test_stop_timeout_matcher_rejects_wrong_lineage_or_terminal_status(
|
|
mismatch: str,
|
|
) -> None:
|
|
stop_operation_id = "op-00000000-0000-4000-8000-000000001489"
|
|
acquisition_id = "acquisition-stop-timeout-proof"
|
|
proof: dict[str, Any] = {
|
|
"status": "unresolved",
|
|
"requires_reconciliation": True,
|
|
"record": {
|
|
"operation_id": stop_operation_id,
|
|
"acquisition_id": acquisition_id,
|
|
"action": "stop",
|
|
"stage": "observing",
|
|
"resolution": None,
|
|
# An exact application response may precede or outlive the local
|
|
# QoS2 callback. Timeout cleanup is local-only in either case.
|
|
"publish_call_returned": None,
|
|
"qos2_completed": False,
|
|
"packet_id": None,
|
|
"application_response": {
|
|
"operation_id": stop_operation_id,
|
|
"action": "stop",
|
|
"success": True,
|
|
"observed_at_utc": "2026-08-10T08:10:02.000Z",
|
|
},
|
|
"last_status": None,
|
|
},
|
|
}
|
|
record = proof["record"]
|
|
if mismatch == "operation":
|
|
record["operation_id"] = "different-stop-operation"
|
|
elif mismatch == "acquisition":
|
|
record["acquisition_id"] = "different-acquisition"
|
|
elif mismatch == "response-operation":
|
|
record["application_response"]["operation_id"] = "different-stop-operation"
|
|
elif mismatch == "response-failed":
|
|
record["application_response"]["success"] = False
|
|
elif mismatch in {"ready", "stopping"}:
|
|
record["last_status"] = {
|
|
"session_state": "ready" if mismatch == "ready" else "scan_stopping"
|
|
}
|
|
|
|
assert (
|
|
XgridsK1CompatibilityService._matching_stop_response_without_terminal_status( # noqa: SLF001
|
|
proof,
|
|
acquisition_id=acquisition_id,
|
|
stop_operation_id=stop_operation_id,
|
|
)
|
|
is False
|
|
)
|
|
|
|
|
|
def test_stop_timeout_matcher_accepts_exact_response_without_qos2_callback() -> None:
|
|
stop_operation_id = "op-00000000-0000-4000-8000-000000001488"
|
|
acquisition_id = "acquisition-stop-timeout-no-qos"
|
|
proof = {
|
|
"status": "unresolved",
|
|
"requires_reconciliation": True,
|
|
"record": {
|
|
"operation_id": stop_operation_id,
|
|
"acquisition_id": acquisition_id,
|
|
"action": "stop",
|
|
"stage": "observing",
|
|
"resolution": None,
|
|
"publish_call_returned": None,
|
|
"qos2_completed": False,
|
|
"packet_id": None,
|
|
"application_response": {
|
|
"operation_id": stop_operation_id,
|
|
"action": "stop",
|
|
"success": True,
|
|
"observed_at_utc": "2026-08-10T08:10:02.000Z",
|
|
},
|
|
"last_status": None,
|
|
},
|
|
}
|
|
|
|
assert XgridsK1CompatibilityService._matching_stop_response_without_terminal_status( # noqa: SLF001
|
|
proof,
|
|
acquisition_id=acquisition_id,
|
|
stop_operation_id=stop_operation_id,
|
|
)
|
|
|
|
|
|
def _stop_response_without_terminal_status_fixture(
|
|
tmp_path: Path,
|
|
*,
|
|
qos2_completed: bool = True,
|
|
application_response: bool = True,
|
|
) -> SimpleNamespace:
|
|
clock_value = [datetime(2026, 8, 10, 8, 10, tzinfo=UTC)]
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
service._operations = OperationJournal(clock=lambda: clock_value[0]) # noqa: SLF001
|
|
control = FakeInteractiveControlSession()
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
binding = _seed_supervised_connection(
|
|
service,
|
|
connection_mode="bridge",
|
|
transport_ref="k1-stop-timeout",
|
|
)
|
|
control.verified_control = _verified_control_for_binding(
|
|
binding,
|
|
control_session_id="stop-timeout-control",
|
|
)
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="STOP_TIMEOUT",
|
|
host=binding.target_ipv4,
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(
|
|
_start_request(
|
|
acquisition_id=acquisition_id,
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
)
|
|
start_operation_id = service._acquisition_start_operation_id # noqa: SLF001
|
|
assert isinstance(start_operation_id, str)
|
|
|
|
identity = PhysicalCommandIdentity(
|
|
vendor_device_id_sha256="4" * 64,
|
|
device_serial_sha256="5" * 64,
|
|
)
|
|
physical_connection = PhysicalCommandConnectionBinding(
|
|
intent_id=binding.intent_id,
|
|
transport_ref=binding.transport_ref,
|
|
connection_mode="bridge",
|
|
target_ipv4=binding.target_ipv4,
|
|
target_port=binding.target_port,
|
|
host_path_epoch=binding.host_path_epoch,
|
|
control_session_id="stop-timeout-control",
|
|
producer_generation=1,
|
|
)
|
|
|
|
def status(
|
|
session_state: str,
|
|
*,
|
|
observed_at_utc: str,
|
|
) -> PhysicalCommandStatusEvidence:
|
|
scanning = session_state == "scanning"
|
|
return PhysicalCommandStatusEvidence(
|
|
source="live-control-session",
|
|
vendor_device_id_sha256=identity.vendor_device_id_sha256,
|
|
device_serial_sha256=identity.device_serial_sha256,
|
|
control_session_id=physical_connection.control_session_id,
|
|
host_path_epoch=physical_connection.host_path_epoch,
|
|
producer_generation=physical_connection.producer_generation,
|
|
session_state=session_state, # type: ignore[arg-type]
|
|
session_state_code=302 if scanning else 300,
|
|
project_bound=scanning,
|
|
project_id_sha256="6" * 64 if scanning else None,
|
|
init_ready=scanning,
|
|
status_message_sha256=hashlib.sha256(
|
|
f"{session_state}:{observed_at_utc}".encode()
|
|
).hexdigest(),
|
|
mqtt_retained=False,
|
|
observed_at_utc=observed_at_utc,
|
|
)
|
|
|
|
ledger = service._physical_command_ledger # noqa: SLF001
|
|
ledger.prepare(
|
|
operation_id=start_operation_id,
|
|
parent_operation_id=None,
|
|
acquisition_id=acquisition_id,
|
|
action="start",
|
|
identity=identity,
|
|
connection=physical_connection,
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
payload_sha256="7" * 64,
|
|
baseline_status=status(
|
|
"ready",
|
|
observed_at_utc="2026-08-10T08:09:55.000Z",
|
|
),
|
|
)
|
|
ledger.mark_dispatching(start_operation_id)
|
|
ledger.mark_observing(
|
|
start_operation_id,
|
|
publish_call_returned=True,
|
|
packet_id=81,
|
|
)
|
|
ledger.mark_qos2_completed(start_operation_id, packet_id=81)
|
|
ledger.record_application_response(
|
|
start_operation_id,
|
|
PhysicalCommandApplicationResponse(
|
|
operation_id=start_operation_id,
|
|
action="start",
|
|
control_session_id=physical_connection.control_session_id,
|
|
host_path_epoch=physical_connection.host_path_epoch,
|
|
producer_generation=physical_connection.producer_generation,
|
|
result_code=PHYSICAL_COMMAND_APPLICATION_SUCCESS_CODE,
|
|
success=True,
|
|
payload_sha256="8" * 64,
|
|
observed_at_utc="2026-08-10T08:09:56.000Z",
|
|
),
|
|
)
|
|
ledger.record_status_observation(
|
|
start_operation_id,
|
|
status("scanning", observed_at_utc="2026-08-10T08:09:57.000Z"),
|
|
)
|
|
ledger.resolve(start_operation_id, resolution="start-active-observed")
|
|
runtime.mark_ready()
|
|
runtime.pcl_frames = 1
|
|
assert service.state()["acquisition"]["state"] == "acquiring"
|
|
|
|
stop_operation_id = "op-00000000-0000-4000-8000-000000001499"
|
|
stop_request = _stop_request(
|
|
acquisition_id=acquisition_id,
|
|
operation_id=stop_operation_id,
|
|
deadline_seconds=10.0,
|
|
mode="graceful",
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
stopping = service.stop_acquisition(stop_request)
|
|
assert stopping["acquisition"]["state"] == "awaiting_external_stop"
|
|
assert stopping["last_operation"]["status"] == "running"
|
|
|
|
ledger.prepare(
|
|
operation_id=stop_operation_id,
|
|
parent_operation_id=start_operation_id,
|
|
acquisition_id=acquisition_id,
|
|
action="stop",
|
|
identity=identity,
|
|
connection=physical_connection,
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
payload_sha256="9" * 64,
|
|
baseline_status=status(
|
|
"scanning",
|
|
observed_at_utc="2026-08-10T08:10:00.000Z",
|
|
),
|
|
)
|
|
ledger.mark_dispatching(stop_operation_id)
|
|
if qos2_completed:
|
|
ledger.mark_observing(
|
|
stop_operation_id,
|
|
publish_call_returned=True,
|
|
packet_id=82,
|
|
)
|
|
ledger.mark_qos2_completed(stop_operation_id, packet_id=82)
|
|
if application_response:
|
|
ledger.record_application_response(
|
|
stop_operation_id,
|
|
PhysicalCommandApplicationResponse(
|
|
operation_id=stop_operation_id,
|
|
action="stop",
|
|
control_session_id=physical_connection.control_session_id,
|
|
host_path_epoch=physical_connection.host_path_epoch,
|
|
producer_generation=physical_connection.producer_generation,
|
|
result_code=PHYSICAL_COMMAND_APPLICATION_SUCCESS_CODE,
|
|
success=True,
|
|
payload_sha256="a" * 64,
|
|
observed_at_utc="2026-08-10T08:10:02.000Z",
|
|
),
|
|
)
|
|
return SimpleNamespace(
|
|
service=service,
|
|
runtime=runtime,
|
|
control=control,
|
|
ledger=ledger,
|
|
clock_value=clock_value,
|
|
acquisition_id=acquisition_id,
|
|
stop_operation_id=stop_operation_id,
|
|
status=status,
|
|
)
|
|
|
|
|
|
def test_unknown_stop_retires_only_terminal_control_and_keeps_live_capture(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
fixture = _stop_response_without_terminal_status_fixture(
|
|
tmp_path,
|
|
qos2_completed=False,
|
|
application_response=False,
|
|
)
|
|
service = fixture.service
|
|
control = fixture.control
|
|
close_calls = 0
|
|
worker_retired = False
|
|
original_snapshot = control.snapshot
|
|
original_close = control.close
|
|
|
|
def terminal_snapshot() -> dict[str, object]:
|
|
snapshot = original_snapshot()
|
|
if control.state == "failed" and not worker_retired:
|
|
snapshot["can_open"] = False
|
|
return snapshot
|
|
|
|
def retire_failed_worker() -> None:
|
|
nonlocal close_calls, worker_retired
|
|
close_calls += 1
|
|
worker_retired = True
|
|
original_close()
|
|
|
|
monkeypatch.setattr(control, "snapshot", terminal_snapshot)
|
|
monkeypatch.setattr(control, "close", retire_failed_worker)
|
|
control.state = "failed"
|
|
control.outcome_unknown = True
|
|
control.failure = {
|
|
"code": "ApplicationCommandOutcomeUnknown",
|
|
"reason_code": "mqtt_response_timeout",
|
|
"stop_command_attempted": True,
|
|
"stop_publish_attempts": 1,
|
|
"safe_to_retry": False,
|
|
}
|
|
service._acquire_application_control_process_lease() # noqa: SLF001
|
|
|
|
recovered = service.state()
|
|
|
|
assert close_calls == 1
|
|
assert recovered["application_control_session"]["state"] == "idle"
|
|
assert recovered["acquisition"]["state"] == "awaiting_external_stop"
|
|
assert recovered["source_mode"] == "live"
|
|
assert recovered["physical_command"]["status"] == "unresolved"
|
|
assert recovered["physical_command"]["requires_reconciliation"] is True
|
|
assert fixture.runtime.stop_calls == 0
|
|
assert control.stop_calls == 1
|
|
assert service._application_control_process_lease_holders == set() # noqa: SLF001
|
|
|
|
|
|
@pytest.mark.parametrize("qos2_completed", [True, False])
|
|
def test_stop_success_without_terminal_status_times_out_into_local_only_recovery(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
qos2_completed: bool,
|
|
) -> None:
|
|
fixture = _stop_response_without_terminal_status_fixture(
|
|
tmp_path,
|
|
qos2_completed=qos2_completed,
|
|
)
|
|
service = fixture.service
|
|
runtime = fixture.runtime
|
|
control = fixture.control
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.deadline_at is not None
|
|
|
|
fixture.clock_value[0] = operation.deadline_at - timedelta(milliseconds=1)
|
|
before_deadline = service.state()
|
|
assert before_deadline["acquisition"]["state"] == "awaiting_external_stop"
|
|
assert service._acquisition_session_lease is not None # noqa: SLF001
|
|
assert before_deadline["source_mode"] == "live"
|
|
assert before_deadline["last_operation"]["status"] == "running"
|
|
assert runtime.stop_calls == 0
|
|
assert control.stop_calls == 1
|
|
|
|
unintended_edges: list[str] = []
|
|
|
|
async def forbidden_device_edge(*_args: object, **_kwargs: object) -> object:
|
|
unintended_edges.append("device-or-network-io")
|
|
raise AssertionError("STOP timeout recovery must be local-only")
|
|
|
|
monkeypatch.setattr(facade_module, "scan", forbidden_device_edge)
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_device_edge)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_device_edge)
|
|
fixture.clock_value[0] = operation.deadline_at
|
|
timed_out = service.state()
|
|
timeout_operation = next(
|
|
item
|
|
for item in timed_out["operations"]
|
|
if item["operation_id"] == fixture.stop_operation_id
|
|
)
|
|
|
|
assert timed_out["application_control_session"]["state"] == "idle"
|
|
assert timed_out["acquisition"]["state"] == "failed"
|
|
assert timed_out["acquisition"]["cleanup_pending"] is False
|
|
assert timed_out["acquisition"]["message_code"] == (
|
|
"acquisition.stop.accepted_physical_outcome_unknown"
|
|
)
|
|
assert timed_out["acquisition"]["result"] == {
|
|
"receiver_stopped": True,
|
|
"device_state": "unknown",
|
|
"device_stop": "stop-accepted-physical-outcome-unknown",
|
|
"automatic_replay_allowed": False,
|
|
}
|
|
assert timeout_operation["status"] == "timed_out"
|
|
assert timeout_operation["stage_code"] == ("stop-accepted-physical-outcome-unknown")
|
|
assert timeout_operation["error"] == {
|
|
"category": "device",
|
|
"code": "stop-accepted-physical-outcome-unknown",
|
|
"retryable": False,
|
|
"safe_to_retry": False,
|
|
"side_effect_status": "unknown",
|
|
"automatic_replay_allowed": False,
|
|
}
|
|
assert timed_out["physical_command"]["status"] == "unresolved"
|
|
assert timed_out["physical_command"]["requires_reconciliation"] is True
|
|
assert timed_out["physical_command"]["record"]["resolution"] is None
|
|
assert timed_out["source_mode"] == "idle"
|
|
assert timed_out["live_perception_shadow"]["active"] is False
|
|
assert timed_out["connection_policy"]["actions"]["scan-ble"]["allowed"] is True
|
|
for action in ("provision-fresh-device", "start-acquisition", "stop-acquisition"):
|
|
assert timed_out["connection_policy"]["actions"][action]["allowed"] is False
|
|
assert unintended_edges == []
|
|
assert runtime.stop_calls == 1
|
|
assert control.stop_calls == 1
|
|
|
|
repeated = service.state()
|
|
assert repeated["acquisition"]["state"] == "failed"
|
|
assert repeated["physical_command"]["status"] == "unresolved"
|
|
assert repeated["connection_policy"]["actions"]["scan-ble"]["allowed"] is True
|
|
assert unintended_edges == []
|
|
assert runtime.stop_calls == 1
|
|
assert control.stop_calls == 1
|
|
|
|
restarted, restarted_runtime = service_with_fake_runtime(tmp_path)
|
|
restarted_state = restarted.state()
|
|
assert restarted_state["physical_command"]["status"] == "unresolved"
|
|
assert restarted_state["physical_command"]["record"]["operation_id"] == (
|
|
fixture.stop_operation_id
|
|
)
|
|
assert restarted_state["acquisition"] is None
|
|
assert restarted_state["connection_policy"]["actions"]["scan-ble"]["allowed"] is True
|
|
for action in ("provision-fresh-device", "start-acquisition", "stop-acquisition"):
|
|
assert restarted_state["connection_policy"]["actions"][action]["allowed"] is False
|
|
assert restarted_runtime.stop_calls == 0
|
|
|
|
|
|
def test_stop_timeout_local_cleanup_failure_retries_only_local_resources(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _stop_response_without_terminal_status_fixture(tmp_path)
|
|
operation = fixture.service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.deadline_at is not None
|
|
fixture.clock_value[0] = operation.deadline_at
|
|
fixture.runtime.stop_error = RuntimeError("synthetic timeout cleanup failure")
|
|
|
|
failed_cleanup = fixture.service.state()
|
|
failed_operation = next(
|
|
item
|
|
for item in failed_cleanup["operations"]
|
|
if item["operation_id"] == fixture.stop_operation_id
|
|
)
|
|
assert failed_cleanup["acquisition"]["state"] == "failed"
|
|
assert failed_cleanup["acquisition"]["cleanup_pending"] is True
|
|
assert failed_cleanup["source_mode"] == "live"
|
|
assert failed_operation["status"] == "failed"
|
|
assert failed_operation["stage_code"] == ("local-cleanup-failed-after-stop-outcome-unknown")
|
|
assert failed_operation["error"]["side_effect_status"] == "unknown"
|
|
assert failed_operation["error"]["safe_to_retry"] is False
|
|
assert failed_operation["error"]["automatic_replay_allowed"] is False
|
|
assert fixture.control.stop_calls == 1
|
|
assert fixture.runtime.stop_calls == 1
|
|
|
|
fixture.runtime.stop_error = None
|
|
recovered = fixture.service.state()
|
|
assert recovered["acquisition"]["state"] == "failed"
|
|
assert recovered["acquisition"]["cleanup_pending"] is False
|
|
assert recovered["acquisition"]["result"]["receiver_stopped"] is True
|
|
assert "local_cleanup_retry_pending" not in recovered["acquisition"]["result"]
|
|
assert recovered["source_mode"] == "idle"
|
|
assert recovered["physical_command"]["status"] == "unresolved"
|
|
assert recovered["connection_policy"]["actions"]["scan-ble"]["allowed"] is True
|
|
assert fixture.control.stop_calls == 1
|
|
assert fixture.runtime.stop_calls == 2
|
|
|
|
repeated = fixture.service.state()
|
|
assert repeated["acquisition"]["cleanup_pending"] is False
|
|
assert fixture.control.stop_calls == 1
|
|
assert fixture.runtime.stop_calls == 2
|
|
|
|
|
|
def test_stop_response_preserves_precise_unknown_outcome_through_predeadline_loss(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
fixture = _stop_response_without_terminal_status_fixture(tmp_path)
|
|
operation = fixture.service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.deadline_at is not None
|
|
original_snapshot = fixture.control.snapshot
|
|
|
|
def failed_control_snapshot() -> dict[str, object]:
|
|
snapshot = original_snapshot()
|
|
if fixture.control.state == "failed":
|
|
snapshot["failure"] = {
|
|
"reason_code": "mqtt_network_loop_failed",
|
|
"safe_to_retry": False,
|
|
"modeling_command_attempted": True,
|
|
}
|
|
return snapshot
|
|
|
|
monkeypatch.setattr(fixture.control, "snapshot", failed_control_snapshot)
|
|
fixture.control.state = "failed"
|
|
fixture.runtime.phase = "error"
|
|
fixture.clock_value[0] = operation.deadline_at - timedelta(milliseconds=1)
|
|
|
|
before_deadline = fixture.service.state()
|
|
stop_operation = next(
|
|
item
|
|
for item in before_deadline["operations"]
|
|
if item["operation_id"] == fixture.stop_operation_id
|
|
)
|
|
assert before_deadline["acquisition"]["state"] == "awaiting_external_stop"
|
|
assert before_deadline["acquisition"]["message_code"] == ("acquisition.stop.device_stopping")
|
|
assert stop_operation["status"] == "running"
|
|
assert before_deadline["physical_command"]["status"] == "unresolved"
|
|
assert fixture.runtime.stop_calls == 0
|
|
assert fixture.control.stop_calls == 1
|
|
|
|
fixture.clock_value[0] = operation.deadline_at
|
|
timed_out = fixture.service.state()
|
|
stop_operation = next(
|
|
item
|
|
for item in timed_out["operations"]
|
|
if item["operation_id"] == fixture.stop_operation_id
|
|
)
|
|
assert timed_out["acquisition"]["message_code"] == (
|
|
"acquisition.stop.accepted_physical_outcome_unknown"
|
|
)
|
|
assert stop_operation["status"] == "timed_out"
|
|
assert stop_operation["stage_code"] == "stop-accepted-physical-outcome-unknown"
|
|
assert timed_out["source_mode"] == "idle"
|
|
assert timed_out["physical_command"]["status"] == "unresolved"
|
|
assert fixture.runtime.stop_calls == 1
|
|
assert fixture.control.stop_calls == 1
|
|
|
|
|
|
def test_stop_ready_resolution_wins_when_it_arrives_at_timeout_deadline(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
fixture = _stop_response_without_terminal_status_fixture(tmp_path)
|
|
operation = fixture.service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.deadline_at is not None
|
|
fixture.clock_value[0] = operation.deadline_at
|
|
fixture.ledger.record_status_observation(
|
|
fixture.stop_operation_id,
|
|
fixture.status("ready", observed_at_utc="2026-08-10T08:10:10.000Z"),
|
|
)
|
|
fixture.ledger.resolve(
|
|
fixture.stop_operation_id,
|
|
resolution="stop-standby-observed",
|
|
)
|
|
fixture.control.state = "idle"
|
|
fixture.control.verified_control = None
|
|
|
|
completed = fixture.service.state()
|
|
completed_operation = next(
|
|
item
|
|
for item in completed["operations"]
|
|
if item["operation_id"] == fixture.stop_operation_id
|
|
)
|
|
assert completed["acquisition"]["state"] == "completed"
|
|
assert completed["acquisition"]["result"]["device_state"] == "ready"
|
|
assert completed_operation["status"] == "succeeded"
|
|
assert completed["physical_command"]["record"]["resolution"] == ("stop-standby-observed")
|
|
assert fixture.control.stop_calls == 1
|
|
assert fixture.runtime.stop_calls == 1
|
|
|
|
|
|
def test_facade_wires_one_physical_command_coordinator_into_control_session(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
|
|
assert (
|
|
service._application_control_session._physical_command_coordinator # noqa: SLF001
|
|
is service._physical_command_coordinator # noqa: SLF001
|
|
)
|
|
physical_snapshot = service.state()["physical_command"]
|
|
assert physical_snapshot["automatic_replay_allowed"] is False
|
|
assert physical_snapshot["status"] == "empty"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("stage", "action", "expected_resolution"),
|
|
[
|
|
(
|
|
"prepared",
|
|
"start",
|
|
[("op-00000000-0000-4000-8000-000000000111", "not-dispatched")],
|
|
),
|
|
("prepared", "stop", []),
|
|
("dispatching", "start", []),
|
|
("observing", "start", []),
|
|
],
|
|
)
|
|
def test_facade_startup_auto_resolves_only_pre_dispatch_physical_attempt(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
stage: str,
|
|
action: str,
|
|
expected_resolution: list[tuple[str, str]],
|
|
) -> None:
|
|
operation_id = "op-00000000-0000-4000-8000-000000000111"
|
|
|
|
class StartupLedger:
|
|
def __init__(self) -> None:
|
|
self.resolutions: list[tuple[str, str]] = []
|
|
|
|
def snapshot(self) -> SimpleNamespace:
|
|
return SimpleNamespace(
|
|
status="unresolved",
|
|
record=SimpleNamespace(
|
|
operation_id=operation_id,
|
|
stage=stage,
|
|
action=action,
|
|
),
|
|
)
|
|
|
|
def resolve(self, candidate: str, *, resolution: str) -> None:
|
|
self.resolutions.append((candidate, resolution))
|
|
|
|
ledger = StartupLedger()
|
|
monkeypatch.setattr(facade_module, "PhysicalCommandLedger", lambda _root: ledger)
|
|
|
|
service = XgridsK1CompatibilityService(
|
|
tmp_path,
|
|
host_wifi_association_probe=FakeHostWifiAssociationProbe(),
|
|
)
|
|
|
|
assert service._physical_command_ledger is ledger # noqa: SLF001
|
|
assert ledger.resolutions == expected_resolution
|
|
|
|
|
|
def test_facade_physical_context_chains_stop_to_durable_start_after_transient_id_clears(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
start_operation_id = "op-00000000-0000-4000-8000-000000000101"
|
|
stop_operation_id = "op-00000000-0000-4000-8000-000000000102"
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control = FakeInteractiveControlSession()
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
binding = _seed_supervised_connection(service)
|
|
service._compatibility_attestation = ATTESTATION.model_dump(mode="json") # noqa: SLF001
|
|
|
|
class FacadeAdmissionLedger:
|
|
def __init__(self) -> None:
|
|
self.record: object | None = None
|
|
self.admission_calls = 0
|
|
|
|
def require_no_unresolved_attempt(self) -> None:
|
|
self.admission_calls += 1
|
|
|
|
def snapshot(self) -> SimpleNamespace:
|
|
return SimpleNamespace(record=self.record)
|
|
|
|
ledger = FacadeAdmissionLedger()
|
|
service._physical_command_ledger = ledger # type: ignore[assignment] # noqa: SLF001
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="TEST001",
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
|
|
service.start_acquisition(
|
|
_start_request(
|
|
acquisition_id=acquisition_id,
|
|
operation_id=start_operation_id,
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
)
|
|
start_context = control.start_contexts[-1]
|
|
assert start_context.operation_id == start_operation_id
|
|
assert start_context.parent_operation_id is None
|
|
assert start_context.acquisition_id == acquisition_id
|
|
assert start_context.operator_confirmation_id is None
|
|
assert start_context.operator_confirmed_at_utc is None
|
|
|
|
ledger.record = SimpleNamespace(operation_id=start_context.operation_id)
|
|
service._physical_command_coordinator = SimpleNamespace( # type: ignore[assignment] # noqa: SLF001
|
|
snapshot=lambda: _exact_start_physical_proof(
|
|
operation_id=start_operation_id,
|
|
acquisition_id=acquisition_id,
|
|
binding=binding,
|
|
resolved=True,
|
|
)
|
|
)
|
|
runtime.mark_ready()
|
|
runtime.pcl_frames = 1
|
|
service.state()
|
|
assert service._acquisition_start_operation_id is None # noqa: SLF001
|
|
|
|
service.stop_acquisition(
|
|
_stop_request(
|
|
acquisition_id=acquisition_id,
|
|
operation_id=stop_operation_id,
|
|
mode="graceful",
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
)
|
|
stop_context = control.stop_contexts[-1]
|
|
assert stop_context.operation_id == stop_operation_id
|
|
assert stop_context.parent_operation_id == start_operation_id
|
|
assert stop_context.acquisition_id == acquisition_id
|
|
assert stop_context.operator_confirmation_id is None
|
|
assert stop_context.operator_confirmed_at_utc is None
|
|
assert ledger.admission_calls == 2
|
|
|
|
|
|
def test_facade_blocks_unresolved_physical_start_before_local_receiver_side_effects(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
operation_id = "op-00000000-0000-4000-8000-000000000103"
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control = FakeInteractiveControlSession()
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
_seed_supervised_connection(service)
|
|
service._compatibility_attestation = ATTESTATION.model_dump(mode="json") # noqa: SLF001
|
|
|
|
class BlockedPhysicalLedger:
|
|
def require_no_unresolved_attempt(self) -> None:
|
|
raise PhysicalCommandBlocked("ambiguous previous physical edge")
|
|
|
|
service._physical_command_ledger = BlockedPhysicalLedger() # type: ignore[assignment] # noqa: SLF001
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="TEST001",
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
|
|
with pytest.raises(PhysicalCommandBlocked):
|
|
service.start_acquisition(
|
|
_start_request(
|
|
acquisition_id=prepared["acquisition"]["acquisition_id"],
|
|
operation_id=operation_id,
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
)
|
|
|
|
assert runtime.start_calls == []
|
|
assert control.start_contexts == []
|
|
assert service._operations.get(operation_id).status == "failed" # noqa: SLF001
|
|
|
|
|
|
def test_plugin_commanded_prepare_rejects_stale_binding_before_creating_acquisition(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
|
|
class StaleInteractiveControlSession(FakeInteractiveControlSession):
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.project_prompt_calls = 0
|
|
self.close_calls = 0
|
|
|
|
def validate_connection_binding(self) -> None:
|
|
raise ApplicationConnectionBindingLost("test route changed")
|
|
|
|
def open_project_prompt(
|
|
self,
|
|
*,
|
|
expected_session_generation: int | None = None,
|
|
expected_state_revision: int | None = None,
|
|
) -> dict[str, object]:
|
|
self.project_prompt_calls += 1
|
|
return super().open_project_prompt(
|
|
expected_session_generation=expected_session_generation,
|
|
expected_state_revision=expected_state_revision,
|
|
)
|
|
|
|
def close_prestart(self) -> dict[str, object]:
|
|
self.close_calls += 1
|
|
return super().close_prestart()
|
|
|
|
control = StaleInteractiveControlSession()
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
_seed_supervised_connection(service)
|
|
service._compatibility_attestation = ATTESTATION.model_dump(mode="json") # noqa: SLF001
|
|
|
|
with pytest.raises(ApplicationConnectionBindingLost):
|
|
service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="TEST001",
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
|
|
assert service._acquisition is None # noqa: SLF001
|
|
assert control.project_prompt_calls == 0
|
|
assert control.close_calls == 1
|
|
assert control.state == "closed"
|
|
|
|
|
|
def test_workspace_entry_rejects_stale_binding_before_releasing_checkpoint(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
|
|
class StaleWorkspaceControlSession(FakeInteractiveControlSession):
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.state = "connection-ready"
|
|
self.enter_calls = 0
|
|
self.close_calls = 0
|
|
|
|
def validate_connection_binding(self) -> None:
|
|
raise ApplicationConnectionBindingLost("test route changed")
|
|
|
|
def enter_workspace(
|
|
self,
|
|
*,
|
|
expected_session_generation: int | None = None,
|
|
expected_state_revision: int | None = None,
|
|
) -> dict[str, object]:
|
|
# Production ``InteractiveApplicationControlSession`` owns this
|
|
# command-bound guard internally. The facade must not duplicate
|
|
# it ahead of the checkpoint.
|
|
self.validate_connection_binding()
|
|
del expected_session_generation, expected_state_revision
|
|
self.enter_calls += 1
|
|
raise AssertionError("stale checkpoint must not be released")
|
|
|
|
def close_prestart(self) -> dict[str, object]:
|
|
self.close_calls += 1
|
|
return super().close_prestart()
|
|
|
|
control = StaleWorkspaceControlSession()
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
_seed_supervised_connection(service)
|
|
|
|
with pytest.raises(ApplicationConnectionBindingLost):
|
|
service.enter_application_workspace(
|
|
EnterApplicationWorkspaceRequest(
|
|
operator_confirmed=True,
|
|
expected_session_generation=1,
|
|
expected_state_revision=1,
|
|
)
|
|
)
|
|
|
|
assert control.enter_calls == 0
|
|
assert control.close_calls == 1
|
|
assert control.state == "closed"
|
|
|
|
|
|
class _BindingValidatingControlSession(FakeInteractiveControlSession):
|
|
"""Test double with the production session's command-bound guards."""
|
|
|
|
def __init__(
|
|
self,
|
|
service: XgridsK1CompatibilityService,
|
|
binding: ApplicationConnectionBinding,
|
|
) -> None:
|
|
super().__init__(initial_state="idle")
|
|
self._service = service
|
|
self._binding = binding
|
|
self.validation_calls = 0
|
|
|
|
def validate_connection_binding(self) -> None:
|
|
self.validation_calls += 1
|
|
self._service._validate_application_connection_binding(self._binding) # noqa: SLF001
|
|
|
|
def enter_workspace(
|
|
self,
|
|
*,
|
|
expected_session_generation: int | None = None,
|
|
expected_state_revision: int | None = None,
|
|
) -> dict[str, object]:
|
|
self.validate_connection_binding()
|
|
return super().enter_workspace(
|
|
expected_session_generation=expected_session_generation,
|
|
expected_state_revision=expected_state_revision,
|
|
)
|
|
|
|
def open_project_prompt(
|
|
self,
|
|
*,
|
|
expected_session_generation: int | None = None,
|
|
expected_state_revision: int | None = None,
|
|
) -> dict[str, object]:
|
|
self.validate_connection_binding()
|
|
return super().open_project_prompt(
|
|
expected_session_generation=expected_session_generation,
|
|
expected_state_revision=expected_state_revision,
|
|
)
|
|
|
|
def request_start(
|
|
self,
|
|
*,
|
|
project_name: str,
|
|
confirmation: object,
|
|
command_context: object,
|
|
preparation_checkpoint_observer: object | None = None,
|
|
expected_session_generation: int | None = None,
|
|
expected_state_revision: int | None = None,
|
|
) -> dict[str, object]:
|
|
self.validate_connection_binding()
|
|
return super().request_start(
|
|
project_name=project_name,
|
|
confirmation=confirmation,
|
|
command_context=command_context,
|
|
preparation_checkpoint_observer=preparation_checkpoint_observer,
|
|
expected_session_generation=expected_session_generation,
|
|
expected_state_revision=expected_state_revision,
|
|
)
|
|
|
|
|
|
def _install_binding_validating_ready_control(
|
|
service: XgridsK1CompatibilityService,
|
|
*,
|
|
host_path: HostPathProbeResult | None = None,
|
|
) -> tuple[
|
|
_BindingValidatingControlSession,
|
|
ApplicationConnectionBinding,
|
|
dict[str, Any],
|
|
]:
|
|
binding = _seed_supervised_connection(
|
|
service,
|
|
connection_mode="bridge",
|
|
transport_ref="ttl-bound-k1",
|
|
logical_device_id="ttl-bound-device",
|
|
host_path=host_path,
|
|
)
|
|
control = _BindingValidatingControlSession(service, binding)
|
|
control.open(connection_binding=binding)
|
|
control.verified_control = _verified_control_for_binding(
|
|
binding,
|
|
logical_device_id="ttl-bound-device",
|
|
control_session_id="test-control-ttl-bound-k1",
|
|
control_proof_revision=2,
|
|
)
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
service._reconcile_connection_supervisor( # noqa: SLF001
|
|
control.snapshot(),
|
|
service.runtime.snapshot(),
|
|
)
|
|
service._acquire_application_control_process_lease() # noqa: SLF001
|
|
state = service.state()
|
|
assert state["application_control_session"]["state"] == "connection-ready"
|
|
return control, binding, state
|
|
|
|
|
|
def test_public_commanded_workflow_refreshes_stable_route_before_ttl_reduction(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
monotonic_now = [100.0]
|
|
suspend_aware_now = [1_000.0]
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
supervisor._monotonic_clock = lambda: monotonic_now[0] # noqa: SLF001
|
|
supervisor._suspend_aware_clock = lambda: suspend_aware_now[0] # noqa: SLF001
|
|
control, binding, ready = _install_binding_validating_ready_control(service)
|
|
stable_path = _direct_host_path(binding.target_ipv4)
|
|
monkeypatch.setattr(service, "_sample_host_path", lambda *_args, **_kwargs: stable_path)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_probe_control_endpoint_socket",
|
|
lambda _target: facade_module.TcpReachabilityProbeResult(reachable=True),
|
|
)
|
|
target = EndpointTarget(binding.target_ipv4, binding.target_port)
|
|
|
|
def advance_past_fifteen_seconds_with_fresh_endpoint() -> None:
|
|
monotonic_now[0] += 10.0
|
|
suspend_aware_now[0] += 10.0
|
|
assert supervisor.observe_endpoint(
|
|
target=target,
|
|
intent_id=binding.intent_id,
|
|
host_path_epoch=binding.host_path_epoch,
|
|
reachable=True,
|
|
)
|
|
monotonic_now[0] += 5.01
|
|
suspend_aware_now[0] += 5.01
|
|
|
|
advance_past_fifteen_seconds_with_fresh_endpoint()
|
|
polled = service.state()
|
|
assert polled["application_control_session"]["state"] == "connection-ready"
|
|
assert supervisor.snapshot().host_path.epoch == binding.host_path_epoch
|
|
workspace = service.enter_application_workspace(
|
|
EnterApplicationWorkspaceRequest(
|
|
operator_confirmed=True,
|
|
expected_session_generation=ready["application_control_session"]["session_generation"],
|
|
expected_state_revision=ready["application_control_session"]["state_revision"],
|
|
)
|
|
)
|
|
assert workspace["application_control_session"]["state"] == "workspace-ready"
|
|
assert supervisor.snapshot().host_path.epoch == binding.host_path_epoch
|
|
assert control.validation_calls == 1
|
|
|
|
# Retain a focused regression for the original 15-second reducer ordering
|
|
# even though the production silence lease now tolerates two monitor
|
|
# passes. Command-bound sampling must still precede stale reduction for
|
|
# any reviewed TTL configuration.
|
|
supervisor._observation_ttl_seconds = 15.0 # noqa: SLF001
|
|
advance_past_fifteen_seconds_with_fresh_endpoint()
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host=binding.target_ipv4,
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_control_session_generation=workspace["application_control_session"][
|
|
"session_generation"
|
|
],
|
|
expected_control_state_revision=workspace["application_control_session"][
|
|
"state_revision"
|
|
],
|
|
)
|
|
)
|
|
assert prepared["application_control_session"]["state"] == "project-ready"
|
|
assert supervisor.snapshot().host_path.epoch == binding.host_path_epoch
|
|
assert control.validation_calls == 3
|
|
|
|
advance_past_fifteen_seconds_with_fresh_endpoint()
|
|
started = service.start_acquisition(
|
|
_start_request(
|
|
acquisition_id=prepared["acquisition"]["acquisition_id"],
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
expected_control_session_generation=prepared["application_control_session"][
|
|
"session_generation"
|
|
],
|
|
expected_control_state_revision=prepared["application_control_session"][
|
|
"state_revision"
|
|
],
|
|
)
|
|
)
|
|
|
|
assert started["acquisition"]["state"] == "starting"
|
|
assert supervisor.snapshot().host_path.epoch == binding.host_path_epoch
|
|
assert supervisor.snapshot().authority.control_allowed is True
|
|
assert control.start_projects == [PROJECT_NAME]
|
|
assert control.validation_calls == 5
|
|
assert len(runtime.start_calls) == 1
|
|
|
|
|
|
def test_existing_control_open_refreshes_before_ttl_reuse_gate(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _runtime = service_with_fake_runtime(tmp_path)
|
|
monotonic_now = [100.0]
|
|
suspend_aware_now = [1_000.0]
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
supervisor._monotonic_clock = lambda: monotonic_now[0] # noqa: SLF001
|
|
supervisor._suspend_aware_clock = lambda: suspend_aware_now[0] # noqa: SLF001
|
|
supervisor._observation_ttl_seconds = 15.0 # noqa: SLF001
|
|
control, binding, _ready = _install_binding_validating_ready_control(service)
|
|
stable_path = _direct_host_path(binding.target_ipv4)
|
|
monkeypatch.setattr(service, "_sample_host_path", lambda *_args, **_kwargs: stable_path)
|
|
|
|
monotonic_now[0] += 10.0
|
|
suspend_aware_now[0] += 10.0
|
|
assert supervisor.observe_endpoint(
|
|
target=EndpointTarget(binding.target_ipv4, binding.target_port),
|
|
intent_id=binding.intent_id,
|
|
host_path_epoch=binding.host_path_epoch,
|
|
reachable=True,
|
|
)
|
|
monotonic_now[0] += 5.01
|
|
suspend_aware_now[0] += 5.01
|
|
|
|
def forbid_reopen_or_recovery() -> tuple[str, dict[str, Any]]:
|
|
raise AssertionError("stable existing control must not reopen or enter BLE recovery")
|
|
|
|
monkeypatch.setattr(service, "_reuse_or_recover_control_target", forbid_reopen_or_recovery)
|
|
reused = 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 reused["application_control_session"]["state"] == "connection-ready"
|
|
assert supervisor.snapshot().host_path.epoch == binding.host_path_epoch
|
|
assert supervisor.snapshot().authority.control_allowed is True
|
|
assert control.validation_calls == 1
|
|
open_operation = next(
|
|
operation
|
|
for operation in reused["operations"]
|
|
if operation["action"] == "application-control.session.open"
|
|
)
|
|
assert open_operation["result"]["connection_lease_reused"] is True
|
|
assert open_operation["result"]["device_write_performed"] is False
|
|
|
|
|
|
def test_existing_control_bootstrap_refreshes_before_ttl_reuse_gate(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _runtime = service_with_fake_runtime(tmp_path)
|
|
monotonic_now = [100.0]
|
|
suspend_aware_now = [1_000.0]
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
supervisor._monotonic_clock = lambda: monotonic_now[0] # noqa: SLF001
|
|
supervisor._suspend_aware_clock = lambda: suspend_aware_now[0] # noqa: SLF001
|
|
supervisor._observation_ttl_seconds = 15.0 # noqa: SLF001
|
|
control, binding, _ready = _install_binding_validating_ready_control(service)
|
|
service._bootstrap_prestart_control_ready_owned = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
XgridsK1CompatibilityService._bootstrap_prestart_control_ready_owned, # noqa: SLF001
|
|
service,
|
|
)
|
|
stable_path = _direct_host_path(binding.target_ipv4)
|
|
monkeypatch.setattr(service, "_sample_host_path", lambda *_args, **_kwargs: stable_path)
|
|
|
|
monotonic_now[0] += 10.0
|
|
suspend_aware_now[0] += 10.0
|
|
assert supervisor.observe_endpoint(
|
|
target=EndpointTarget(binding.target_ipv4, binding.target_port),
|
|
intent_id=binding.intent_id,
|
|
host_path_epoch=binding.host_path_epoch,
|
|
reachable=True,
|
|
)
|
|
monotonic_now[0] += 5.01
|
|
suspend_aware_now[0] += 5.01
|
|
|
|
async def forbid_retirement() -> None:
|
|
raise AssertionError("stable existing control must not be retired or reopened")
|
|
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_retire_prestart_control_for_mode_transition",
|
|
forbid_retirement,
|
|
)
|
|
asyncio.run(
|
|
service._bootstrap_prestart_control_ready_owned( # noqa: SLF001
|
|
parent_operation_id=None,
|
|
connection_mode="bridge",
|
|
)
|
|
)
|
|
|
|
assert control.state == "connection-ready"
|
|
assert control.validation_calls == 1
|
|
assert supervisor.snapshot().host_path.epoch == binding.host_path_epoch
|
|
assert supervisor.snapshot().authority.control_allowed is True
|
|
bootstrap = [
|
|
operation
|
|
for operation in service._operations.snapshot() # noqa: SLF001
|
|
if operation["action"] == facade_module.ACTION_CONNECTION_CONTROL_BOOTSTRAP
|
|
]
|
|
assert len(bootstrap) == 1
|
|
assert bootstrap[0]["status"] == "succeeded"
|
|
|
|
|
|
def test_workspace_route_change_still_fails_closed_before_checkpoint(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
monotonic_now = [100.0]
|
|
suspend_aware_now = [1_000.0]
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
supervisor._monotonic_clock = lambda: monotonic_now[0] # noqa: SLF001
|
|
supervisor._suspend_aware_clock = lambda: suspend_aware_now[0] # noqa: SLF001
|
|
control, binding, ready = _install_binding_validating_ready_control(service)
|
|
changed_path = HostPathProbeResult(
|
|
available=True,
|
|
fingerprint="test-route:changed-before-workspace",
|
|
interface="test0",
|
|
source_ipv4="192.168.1.2",
|
|
route_class="direct",
|
|
kernel_route_fingerprint="test-route:changed-before-workspace",
|
|
)
|
|
monkeypatch.setattr(service, "_sample_host_path", lambda *_args, **_kwargs: changed_path)
|
|
|
|
with pytest.raises(ApplicationConnectionBindingLost):
|
|
service.enter_application_workspace(
|
|
EnterApplicationWorkspaceRequest(
|
|
operator_confirmed=True,
|
|
expected_session_generation=ready["application_control_session"][
|
|
"session_generation"
|
|
],
|
|
expected_state_revision=ready["application_control_session"]["state_revision"],
|
|
)
|
|
)
|
|
|
|
assert control.state == "closed"
|
|
assert supervisor.snapshot().host_path.epoch != binding.host_path_epoch
|
|
assert supervisor.snapshot().authority.control_allowed is False
|
|
assert runtime.start_calls == []
|
|
assert control.start_projects == []
|
|
|
|
|
|
def test_start_stale_endpoint_fails_before_receiver_or_device_checkpoint(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, binding, ready = _install_binding_validating_ready_control(service)
|
|
stable_path = _direct_host_path(binding.target_ipv4)
|
|
monkeypatch.setattr(service, "_sample_host_path", lambda *_args, **_kwargs: stable_path)
|
|
workspace = service.enter_application_workspace(
|
|
EnterApplicationWorkspaceRequest(
|
|
operator_confirmed=True,
|
|
expected_session_generation=ready["application_control_session"]["session_generation"],
|
|
expected_state_revision=ready["application_control_session"]["state_revision"],
|
|
)
|
|
)
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host=binding.target_ipv4,
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_control_session_generation=workspace["application_control_session"][
|
|
"session_generation"
|
|
],
|
|
expected_control_state_revision=workspace["application_control_session"][
|
|
"state_revision"
|
|
],
|
|
)
|
|
)
|
|
target = EndpointTarget(binding.target_ipv4, binding.target_port)
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
assert supervisor.observe_endpoint(
|
|
target=target,
|
|
intent_id=binding.intent_id,
|
|
host_path_epoch=binding.host_path_epoch,
|
|
reachable=False,
|
|
reason_code="tcp-endpoint-unreachable",
|
|
)
|
|
physical_before = service._physical_command_ledger.snapshot() # noqa: SLF001
|
|
|
|
with pytest.raises(ApplicationConnectionBindingLost):
|
|
service.start_acquisition(
|
|
_start_request(
|
|
acquisition_id=prepared["acquisition"]["acquisition_id"],
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
expected_control_session_generation=prepared["application_control_session"][
|
|
"session_generation"
|
|
],
|
|
expected_control_state_revision=prepared["application_control_session"][
|
|
"state_revision"
|
|
],
|
|
)
|
|
)
|
|
|
|
assert prepared["acquisition"]["state"] == "prepared"
|
|
assert service._acquisition is not None # noqa: SLF001
|
|
assert service._acquisition.state == "prepared" # noqa: SLF001
|
|
assert service._physical_command_ledger.snapshot() == physical_before # noqa: SLF001
|
|
assert not any(
|
|
operation["action"] == facade_module.ACTION_ACQUISITION_START
|
|
for operation in service._operations.snapshot() # noqa: SLF001
|
|
)
|
|
assert runtime.start_calls == []
|
|
assert control.start_projects == []
|
|
|
|
|
|
def test_start_refreshes_ttl_expired_endpoint_after_project_prepare(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
monotonic_now = [100.0]
|
|
suspend_aware_now = [1_000.0]
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
supervisor._monotonic_clock = lambda: monotonic_now[0] # noqa: SLF001
|
|
supervisor._suspend_aware_clock = lambda: suspend_aware_now[0] # noqa: SLF001
|
|
supervisor._observation_ttl_seconds = 15.0 # noqa: SLF001
|
|
control, binding, ready = _install_binding_validating_ready_control(service)
|
|
stable_path = _direct_host_path(binding.target_ipv4)
|
|
monkeypatch.setattr(service, "_sample_host_path", lambda *_args, **_kwargs: stable_path)
|
|
workspace = service.enter_application_workspace(
|
|
EnterApplicationWorkspaceRequest(
|
|
operator_confirmed=True,
|
|
expected_session_generation=ready["application_control_session"][
|
|
"session_generation"
|
|
],
|
|
expected_state_revision=ready["application_control_session"]["state_revision"],
|
|
)
|
|
)
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host=binding.target_ipv4,
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_control_session_generation=workspace["application_control_session"][
|
|
"session_generation"
|
|
],
|
|
expected_control_state_revision=workspace["application_control_session"][
|
|
"state_revision"
|
|
],
|
|
)
|
|
)
|
|
monotonic_now[0] += 15.01
|
|
suspend_aware_now[0] += 15.01
|
|
assert control.verified_control is not None
|
|
control.verified_control = {
|
|
**control.verified_control,
|
|
"control_proof_revision": 3,
|
|
}
|
|
tcp_samples: list[str] = []
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_probe_control_endpoint_socket",
|
|
lambda target: (
|
|
tcp_samples.append(target)
|
|
or facade_module.TcpReachabilityProbeResult(reachable=True)
|
|
),
|
|
)
|
|
|
|
started = service.start_acquisition(
|
|
_start_request(
|
|
acquisition_id=prepared["acquisition"]["acquisition_id"],
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
expected_control_session_generation=prepared["application_control_session"][
|
|
"session_generation"
|
|
],
|
|
expected_control_state_revision=prepared["application_control_session"][
|
|
"state_revision"
|
|
],
|
|
)
|
|
)
|
|
|
|
assert started["acquisition"]["state"] == "starting"
|
|
assert tcp_samples == [binding.target_ipv4]
|
|
assert supervisor.snapshot().host_path.epoch == binding.host_path_epoch
|
|
assert supervisor.snapshot().endpoint.tcp_state == "reachable"
|
|
assert supervisor.snapshot().authority.acquisition_start_allowed is True
|
|
assert len(runtime.start_calls) == 1
|
|
assert control.start_projects == [PROJECT_NAME]
|
|
|
|
|
|
def test_start_refreshes_endpoint_before_prepared_worker_handoff_window(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
monotonic_now = [100.0]
|
|
suspend_aware_now = [1_000.0]
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
supervisor._monotonic_clock = lambda: monotonic_now[0] # noqa: SLF001
|
|
supervisor._suspend_aware_clock = lambda: suspend_aware_now[0] # noqa: SLF001
|
|
supervisor._observation_ttl_seconds = 15.0 # noqa: SLF001
|
|
control, binding, ready = _install_binding_validating_ready_control(service)
|
|
stable_path = _direct_host_path(binding.target_ipv4)
|
|
monkeypatch.setattr(service, "_sample_host_path", lambda *_args, **_kwargs: stable_path)
|
|
workspace = service.enter_application_workspace(
|
|
EnterApplicationWorkspaceRequest(
|
|
operator_confirmed=True,
|
|
expected_session_generation=ready["application_control_session"][
|
|
"session_generation"
|
|
],
|
|
expected_state_revision=ready["application_control_session"]["state_revision"],
|
|
)
|
|
)
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host=binding.target_ipv4,
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_control_session_generation=workspace["application_control_session"][
|
|
"session_generation"
|
|
],
|
|
expected_control_state_revision=workspace["application_control_session"][
|
|
"state_revision"
|
|
],
|
|
)
|
|
)
|
|
# The endpoint is still nominally reachable, but only eleven seconds of
|
|
# its lease remain. That is insufficient for PREPARED + worker dispatch.
|
|
monotonic_now[0] += 4.01
|
|
suspend_aware_now[0] += 4.01
|
|
assert supervisor.snapshot().endpoint.tcp_state == "reachable"
|
|
tcp_samples: list[str] = []
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_probe_control_endpoint_socket",
|
|
lambda target: (
|
|
tcp_samples.append(target)
|
|
or facade_module.TcpReachabilityProbeResult(reachable=True)
|
|
),
|
|
)
|
|
|
|
started = service.start_acquisition(
|
|
_start_request(
|
|
acquisition_id=prepared["acquisition"]["acquisition_id"],
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
expected_control_session_generation=prepared["application_control_session"][
|
|
"session_generation"
|
|
],
|
|
expected_control_state_revision=prepared["application_control_session"][
|
|
"state_revision"
|
|
],
|
|
)
|
|
)
|
|
|
|
assert started["acquisition"]["state"] == "starting"
|
|
assert tcp_samples == [binding.target_ipv4]
|
|
assert supervisor.endpoint_observation_has_remaining_lease(
|
|
target=EndpointTarget(binding.target_ipv4, binding.target_port),
|
|
intent_id=binding.intent_id,
|
|
host_path_epoch=binding.host_path_epoch,
|
|
minimum_remaining_seconds=facade_module.COMMAND_ENDPOINT_MIN_REMAINING_SECONDS,
|
|
)
|
|
assert len(runtime.start_calls) == 1
|
|
assert control.start_projects == [PROJECT_NAME]
|
|
|
|
|
|
def test_start_stale_control_proof_fails_before_receiver_or_device_checkpoint(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, binding, ready = _install_binding_validating_ready_control(service)
|
|
stable_path = _direct_host_path(binding.target_ipv4)
|
|
monkeypatch.setattr(service, "_sample_host_path", lambda *_args, **_kwargs: stable_path)
|
|
workspace = service.enter_application_workspace(
|
|
EnterApplicationWorkspaceRequest(
|
|
operator_confirmed=True,
|
|
expected_session_generation=ready["application_control_session"]["session_generation"],
|
|
expected_state_revision=ready["application_control_session"]["state_revision"],
|
|
)
|
|
)
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host=binding.target_ipv4,
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_control_session_generation=workspace["application_control_session"][
|
|
"session_generation"
|
|
],
|
|
expected_control_state_revision=workspace["application_control_session"][
|
|
"state_revision"
|
|
],
|
|
)
|
|
)
|
|
|
|
def reject_stale_control_proof(_control: object) -> None:
|
|
raise ApplicationConnectionBindingLost("test MQTT control proof expired")
|
|
|
|
control.validate_connection_binding = MethodType( # type: ignore[method-assign]
|
|
reject_stale_control_proof,
|
|
control,
|
|
)
|
|
physical_before = service._physical_command_ledger.snapshot() # noqa: SLF001
|
|
|
|
with pytest.raises(ApplicationConnectionBindingLost):
|
|
service.start_acquisition(
|
|
_start_request(
|
|
acquisition_id=prepared["acquisition"]["acquisition_id"],
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
expected_control_session_generation=prepared["application_control_session"][
|
|
"session_generation"
|
|
],
|
|
expected_control_state_revision=prepared["application_control_session"][
|
|
"state_revision"
|
|
],
|
|
)
|
|
)
|
|
|
|
assert prepared["acquisition"]["state"] == "prepared"
|
|
assert service._acquisition is not None # noqa: SLF001
|
|
assert service._acquisition.state == "prepared" # noqa: SLF001
|
|
assert service._physical_command_ledger.snapshot() == physical_before # noqa: SLF001
|
|
assert not any(
|
|
operation["action"] == facade_module.ACTION_ACQUISITION_START
|
|
for operation in service._operations.snapshot() # noqa: SLF001
|
|
)
|
|
assert runtime.start_calls == []
|
|
assert control.start_projects == []
|
|
|
|
|
|
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(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
|
|
state = service.start_acquisition(
|
|
_start_request(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_waits_for_first_authoritative_pcl_before_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
|
|
_seed_supervised_connection(service)
|
|
service._compatibility_attestation = ATTESTATION.model_dump(mode="json") # noqa: SLF001
|
|
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="TEST001",
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
pre_pcl_state = service.start_acquisition(
|
|
_start_request(
|
|
acquisition_id=acquisition_id,
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
)
|
|
pre_pcl_camera_streams = [
|
|
stream
|
|
for stream in pre_pcl_state["sensor_catalog"]["streams"]
|
|
if stream.get("semantic_channel_id") == "camera.preview.live"
|
|
]
|
|
assert pre_pcl_state["camera_preview"]["active_source_id"] is None
|
|
assert pre_pcl_state["camera_preview"]["delivery"] is None
|
|
assert pre_pcl_state["camera_preview"]["activation_admission"]["state"] == (
|
|
"waiting-for-first-authoritative-pcl"
|
|
)
|
|
assert all(stream["activation"]["selected"] is False for stream in pre_pcl_camera_streams)
|
|
assert all(stream["activation"]["controllable"] is False for stream in pre_pcl_camera_streams)
|
|
out_dir = service._acquisition_out_dir # noqa: SLF001
|
|
assert out_dir is not None
|
|
out_dir.mkdir(parents=True)
|
|
events: list[tuple[str, object]] = []
|
|
activation_entered = threading.Event()
|
|
release_activation = threading.Event()
|
|
camera_state: dict[str, object] = {
|
|
"phase": "idle",
|
|
"active_source_id": None,
|
|
"recording": {"active": False},
|
|
}
|
|
|
|
def activate_recording(
|
|
source_id: str,
|
|
target: str,
|
|
session_dir: Path,
|
|
*,
|
|
commit_fence: Callable[[Callable[[], bool]], bool],
|
|
committed_before_start: Callable[[dict[str, object]], None] | None = None,
|
|
) -> dict[str, object]:
|
|
events.append(("select", (source_id, target)))
|
|
camera_state["active_source_id"] = source_id
|
|
events.append(("record", session_dir))
|
|
camera_state["recording"] = {
|
|
"active": True,
|
|
"session": session_dir.name,
|
|
"active_epoch": 1,
|
|
"producer_alive": True,
|
|
"last_segment_age_ms": 0,
|
|
"last_media_segment_age_ms": 0,
|
|
"media_ready": True,
|
|
"current_epoch": {
|
|
"generation": 1,
|
|
"init_committed": True,
|
|
"init_committed_age_ms": 0,
|
|
"first_media_committed": True,
|
|
"first_media_committed_age_ms": 0,
|
|
"committed_media_segment_count": 1,
|
|
"last_media_segment_age_ms": 0,
|
|
},
|
|
}
|
|
camera_state["phase"] = "streaming"
|
|
committed = dict(camera_state)
|
|
assert committed_before_start is not None
|
|
assert commit_fence(
|
|
lambda: committed_before_start(committed) is None
|
|
) is True
|
|
return committed
|
|
|
|
actual_camera_snapshot = service.camera_preview.snapshot()
|
|
|
|
def snapshot_camera() -> dict[str, object]:
|
|
return {
|
|
**actual_camera_snapshot,
|
|
**camera_state,
|
|
"recording": {
|
|
**actual_camera_snapshot["recording"],
|
|
**camera_state["recording"], # type: ignore[arg-type]
|
|
},
|
|
}
|
|
|
|
monkeypatch.setattr(service.camera_preview, "snapshot", snapshot_camera)
|
|
monkeypatch.setattr(
|
|
service.camera_preview,
|
|
"activate_recording_producer",
|
|
activate_recording,
|
|
)
|
|
real_activate = service._activate_default_acquisition_camera # noqa: SLF001
|
|
|
|
def gated_activate(**kwargs: object) -> bool:
|
|
activation_entered.set()
|
|
assert release_activation.wait(timeout=2.0)
|
|
return real_activate(**kwargs) # type: ignore[arg-type]
|
|
|
|
monkeypatch.setattr(service, "_activate_default_acquisition_camera", gated_activate)
|
|
|
|
runtime.mark_ready()
|
|
assert control.state == "scanning"
|
|
assert camera_state["active_source_id"] is None
|
|
assert events == []
|
|
|
|
with service._lock: # noqa: SLF001
|
|
start_operation_id = service._acquisition_start_operation_id # noqa: SLF001
|
|
assert start_operation_id is not None
|
|
binding = _seed_supervised_connection(service)
|
|
physical = _exact_start_physical_proof(
|
|
operation_id=start_operation_id,
|
|
acquisition_id=acquisition_id,
|
|
binding=binding,
|
|
resolved=True,
|
|
)
|
|
physical_snapshot_calls = 0
|
|
|
|
def snapshot_physical() -> dict[str, object]:
|
|
nonlocal physical_snapshot_calls
|
|
physical_snapshot_calls += 1
|
|
return physical
|
|
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"snapshot",
|
|
snapshot_physical,
|
|
)
|
|
runtime.pcl_frames = 1
|
|
frame = DecodedPointCloudView(
|
|
context=ConsumerFrameContext(
|
|
sequence=1,
|
|
captured_at_epoch_ns=1,
|
|
received_monotonic_ns=1,
|
|
processing_started_monotonic_ns=1,
|
|
encoded_size_bytes=16,
|
|
live=True,
|
|
),
|
|
frame_id="map",
|
|
positions_xyz=((0.0, 0.0, 0.0),),
|
|
)
|
|
|
|
service._observe_published_runtime_envelope( # noqa: SLF001
|
|
frame,
|
|
runtime.producer_generation,
|
|
)
|
|
# The first PCL admits only the one automatic right-camera activation.
|
|
# Public manual controls stay closed until that worker proves an exact
|
|
# recording epoch, preventing a left/right selection race.
|
|
assert activation_entered.wait(timeout=2.0)
|
|
activating_state = service.state()
|
|
activating_camera_streams = [
|
|
stream
|
|
for stream in activating_state["sensor_catalog"]["streams"]
|
|
if stream.get("semantic_channel_id") == "camera.preview.live"
|
|
]
|
|
assert activating_state["camera_preview"]["activation_admission"]["state"] == "activating"
|
|
assert all(
|
|
stream["activation"]["controllable"] is False
|
|
for stream in activating_camera_streams
|
|
)
|
|
with pytest.raises(RuntimeError, match="автоматического запуска правой камеры"):
|
|
service.select_camera_preview(
|
|
CameraPreviewSelectRequest(
|
|
device_session_id=service._device_session_id, # type: ignore[arg-type] # noqa: SLF001
|
|
source_id="sensor.camera.left",
|
|
)
|
|
)
|
|
assert events == []
|
|
release_activation.set()
|
|
deadline = time.monotonic() + 2.0
|
|
while len(events) < 2 and time.monotonic() < deadline:
|
|
time.sleep(0.01)
|
|
|
|
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,
|
|
"session": out_dir.name,
|
|
"active_epoch": 1,
|
|
"producer_alive": True,
|
|
"last_segment_age_ms": 0,
|
|
"last_media_segment_age_ms": 0,
|
|
"media_ready": True,
|
|
"current_epoch": {
|
|
"generation": 1,
|
|
"init_committed": True,
|
|
"init_committed_age_ms": 0,
|
|
"first_media_committed": True,
|
|
"first_media_committed_age_ms": 0,
|
|
"committed_media_segment_count": 1,
|
|
"last_media_segment_age_ms": 0,
|
|
},
|
|
}
|
|
assert runtime.source_mode == "live"
|
|
assert control.start_projects == ["TEST001"]
|
|
assert control.stop_calls == 0
|
|
|
|
admitted_state = service.state()
|
|
admitted_camera_streams = [
|
|
stream
|
|
for stream in admitted_state["sensor_catalog"]["streams"]
|
|
if stream.get("semantic_channel_id") == "camera.preview.live"
|
|
]
|
|
assert admitted_state["camera_preview"]["activation_admission"]["state"] == "admitted"
|
|
assert any(
|
|
stream["source_id"] == "sensor.camera.right"
|
|
and stream["activation"]["selected"] is True
|
|
and stream["activation"]["controllable"] is True
|
|
for stream in admitted_camera_streams
|
|
)
|
|
|
|
# Every later authoritative PCL for the same lineage is idempotent.
|
|
physical_snapshot_calls_before = physical_snapshot_calls
|
|
service._observe_published_runtime_envelope(frame, runtime.producer_generation) # noqa: SLF001
|
|
time.sleep(0.05)
|
|
assert len(events) == 2
|
|
assert physical_snapshot_calls == physical_snapshot_calls_before
|
|
|
|
|
|
def test_stale_post_publish_pcl_cannot_activate_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
|
|
binding = _seed_supervised_connection(service)
|
|
service._compatibility_attestation = ATTESTATION.model_dump(mode="json") # noqa: SLF001
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="STALEPCL001",
|
|
host=binding.target_ipv4,
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
service.start_acquisition(
|
|
_start_request(
|
|
acquisition_id=prepared["acquisition"]["acquisition_id"],
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
)
|
|
runtime.mark_ready()
|
|
runtime.pcl_frames = 1
|
|
selected: list[object] = []
|
|
monkeypatch.setattr(
|
|
service.camera_preview,
|
|
"select",
|
|
lambda *_args, **_kwargs: selected.append(object()),
|
|
)
|
|
frame = DecodedPointCloudView(
|
|
context=ConsumerFrameContext(
|
|
sequence=1,
|
|
captured_at_epoch_ns=1,
|
|
received_monotonic_ns=1,
|
|
processing_started_monotonic_ns=1,
|
|
encoded_size_bytes=16,
|
|
live=True,
|
|
),
|
|
frame_id="map",
|
|
positions_xyz=((0.0, 0.0, 0.0),),
|
|
)
|
|
|
|
service._observe_published_runtime_envelope( # noqa: SLF001
|
|
frame,
|
|
runtime.producer_generation - 1,
|
|
)
|
|
with service._lock: # noqa: SLF001
|
|
service._acquisition.transition("interrupted", message_code="test.stale") # type: ignore[union-attr] # noqa: SLF001
|
|
service._observe_published_runtime_envelope(frame, runtime.producer_generation) # noqa: SLF001
|
|
time.sleep(0.05)
|
|
|
|
assert selected == []
|
|
assert service.camera_preview.snapshot()["active_source_id"] is None
|
|
|
|
|
|
def test_failed_post_pcl_camera_activation_retries_on_later_authoritative_frame(
|
|
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
|
|
binding = _seed_supervised_connection(service)
|
|
service._compatibility_attestation = ATTESTATION.model_dump(mode="json") # noqa: SLF001
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="RETRYPCL001",
|
|
host=binding.target_ipv4,
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(
|
|
_start_request(
|
|
acquisition_id=acquisition_id,
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
)
|
|
runtime.mark_ready()
|
|
runtime.pcl_frames = 1
|
|
with service._lock: # noqa: SLF001
|
|
start_operation_id = service._acquisition_start_operation_id # noqa: SLF001
|
|
out_dir = service._acquisition_out_dir # noqa: SLF001
|
|
assert start_operation_id is not None
|
|
assert out_dir is not None
|
|
physical = _exact_start_physical_proof(
|
|
operation_id=start_operation_id,
|
|
acquisition_id=acquisition_id,
|
|
binding=_seed_supervised_connection(service),
|
|
resolved=True,
|
|
)
|
|
monkeypatch.setattr(service._physical_command_coordinator, "snapshot", lambda: physical) # noqa: SLF001
|
|
out_dir.mkdir(parents=True)
|
|
actual_camera = service.camera_preview.snapshot()
|
|
camera_state: dict[str, object] = {
|
|
**actual_camera,
|
|
"phase": "idle",
|
|
"generation": None,
|
|
"active_source_id": None,
|
|
"recording": {
|
|
**actual_camera["recording"],
|
|
"active": False,
|
|
"session": None,
|
|
"active_epoch": None,
|
|
"producer_alive": False,
|
|
},
|
|
"error": None,
|
|
}
|
|
select_calls: list[tuple[str, str]] = []
|
|
recording_calls: list[Path] = []
|
|
retry_calls: list[tuple[str, str, int, str]] = []
|
|
|
|
def snapshot_camera() -> dict[str, object]:
|
|
return {
|
|
**camera_state,
|
|
"recording": dict(camera_state["recording"]), # type: ignore[arg-type]
|
|
}
|
|
|
|
def fail_first_activation(
|
|
source_id: str,
|
|
target: str,
|
|
session_dir: Path,
|
|
*,
|
|
commit_fence: Callable[[Callable[[], bool]], bool],
|
|
committed_before_start: Callable[[dict[str, object]], None] | None = None,
|
|
) -> dict[str, object]:
|
|
del commit_fence, committed_before_start
|
|
select_calls.append((source_id, target))
|
|
recording_calls.append(session_dir)
|
|
camera_state.update(
|
|
{
|
|
"phase": "error",
|
|
"error": {
|
|
"code": "ffmpeg-start-failed",
|
|
"message": "synthetic first local spawn failure",
|
|
},
|
|
"generation": 1,
|
|
"active_source_id": source_id,
|
|
"recording": {
|
|
**camera_state["recording"], # type: ignore[arg-type]
|
|
"active": True,
|
|
"session": session_dir.name,
|
|
"active_epoch": None,
|
|
"producer_alive": False,
|
|
},
|
|
}
|
|
)
|
|
raise RuntimeError("synthetic first local spawn failure")
|
|
|
|
def retry_recording(
|
|
source_id: str,
|
|
target: str,
|
|
*,
|
|
expected_generation: int,
|
|
expected_recording_session: str,
|
|
pre_retry_fence: Callable[[Callable[[], bool]], bool],
|
|
commit_fence: Callable[[Callable[[], bool]], bool] | None = None,
|
|
committed_before_start: Callable[[dict[str, object]], None] | None = None,
|
|
) -> dict[str, object]:
|
|
retry_calls.append(
|
|
(source_id, target, expected_generation, expected_recording_session)
|
|
)
|
|
assert camera_state["phase"] == "error"
|
|
assert camera_state["generation"] == expected_generation
|
|
assert pre_retry_fence(lambda: True) is True
|
|
recording = camera_state["recording"]
|
|
assert isinstance(recording, dict)
|
|
assert recording["session"] == expected_recording_session
|
|
camera_state.update(
|
|
{
|
|
"phase": "connecting",
|
|
"generation": expected_generation + 1,
|
|
"error": None,
|
|
"recording": {
|
|
**recording,
|
|
"active_epoch": expected_generation + 1,
|
|
"producer_alive": True,
|
|
"last_segment_age_ms": None,
|
|
"last_media_segment_age_ms": None,
|
|
"media_ready": False,
|
|
"current_epoch": {
|
|
"generation": expected_generation + 1,
|
|
"init_committed": False,
|
|
"init_committed_age_ms": None,
|
|
"first_media_committed": False,
|
|
"first_media_committed_age_ms": None,
|
|
"committed_media_segment_count": 0,
|
|
"last_media_segment_age_ms": None,
|
|
},
|
|
},
|
|
}
|
|
)
|
|
committed = snapshot_camera()
|
|
assert commit_fence is not None
|
|
assert committed_before_start is not None
|
|
assert commit_fence(
|
|
lambda: committed_before_start(committed) is None
|
|
) is True
|
|
return committed
|
|
|
|
monkeypatch.setattr(service.camera_preview, "snapshot", snapshot_camera)
|
|
monkeypatch.setattr(
|
|
service.camera_preview,
|
|
"activate_recording_producer",
|
|
fail_first_activation,
|
|
)
|
|
monkeypatch.setattr(
|
|
service.camera_preview,
|
|
"retry_recording_producer",
|
|
retry_recording,
|
|
)
|
|
frame = DecodedPointCloudView(
|
|
context=ConsumerFrameContext(
|
|
sequence=1,
|
|
captured_at_epoch_ns=1,
|
|
received_monotonic_ns=1,
|
|
processing_started_monotonic_ns=1,
|
|
encoded_size_bytes=16,
|
|
live=True,
|
|
),
|
|
frame_id="map",
|
|
positions_xyz=((0.0, 0.0, 0.0),),
|
|
)
|
|
|
|
service._observe_published_runtime_envelope(frame, runtime.producer_generation) # noqa: SLF001
|
|
deadline = time.monotonic() + 2.0
|
|
while len(recording_calls) < 1 and time.monotonic() < deadline:
|
|
time.sleep(0.01)
|
|
assert recording_calls == [out_dir]
|
|
assert select_calls == [("sensor.camera.right", binding.target_ipv4)]
|
|
|
|
# Normal UI polling between attempts must preserve PCL/Rerun and the
|
|
# scanner-owned session while this exact startup error is retryable.
|
|
provisional = service.state()
|
|
assert provisional["acquisition"]["state"] in {
|
|
"starting",
|
|
"awaiting_external_start",
|
|
"acquiring",
|
|
}
|
|
assert provisional["camera_preview"]["activation_admission"]["state"] == "activating"
|
|
assert runtime.source_mode == "live"
|
|
assert runtime.stop_calls == 0
|
|
assert control.stop_calls == 0
|
|
|
|
# Frames inside the bounded retry window cannot create a worker storm.
|
|
service._observe_published_runtime_envelope(frame, runtime.producer_generation) # noqa: SLF001
|
|
time.sleep(0.05)
|
|
assert retry_calls == []
|
|
|
|
with service._lock: # noqa: SLF001
|
|
service._camera_activation_retry_not_before_monotonic = 0.0 # noqa: SLF001
|
|
service._observe_published_runtime_envelope(frame, runtime.producer_generation) # noqa: SLF001
|
|
deadline = time.monotonic() + 2.0
|
|
while len(retry_calls) < 1 and time.monotonic() < deadline:
|
|
time.sleep(0.01)
|
|
|
|
assert retry_calls == [
|
|
(
|
|
"sensor.camera.right",
|
|
binding.target_ipv4,
|
|
1,
|
|
out_dir.name,
|
|
)
|
|
]
|
|
assert camera_state["generation"] == 2
|
|
recording = camera_state["recording"]
|
|
assert isinstance(recording, dict)
|
|
assert recording["active_epoch"] == 2
|
|
assert recording["producer_alive"] is True
|
|
with service._lock: # noqa: SLF001
|
|
assert service._camera_activation_lineage == ( # noqa: SLF001
|
|
acquisition_id,
|
|
out_dir.name,
|
|
runtime.producer_generation,
|
|
)
|
|
assert control.start_projects == ["RETRYPCL001"]
|
|
assert control.stop_calls == 0
|
|
admitted = service.state()
|
|
assert admitted["acquisition"]["state"] == "acquiring"
|
|
assert admitted["camera_preview"]["activation_admission"]["state"] == "activating"
|
|
assert admitted["camera_preview"]["recording"]["media_ready"] is False
|
|
assert admitted["connection_policy"]["actions"]["stop-acquisition"]["allowed"] is True
|
|
|
|
# The same shaped error without the exact retry claim stays terminal; the
|
|
# classifier never masks storage/archive failures or an unrelated lineage.
|
|
with service._lock: # noqa: SLF001
|
|
service._camera_activation_lineage = None # noqa: SLF001
|
|
service._camera_activation_retry_lineage = None # noqa: SLF001
|
|
monkeypatch.setattr(service, "_seal_acquisition_capture_clock", lambda: None)
|
|
camera_state.update(
|
|
{
|
|
"phase": "error",
|
|
"error": {
|
|
"code": "camera-storage-failed",
|
|
"message": "synthetic durable archive failure",
|
|
},
|
|
"recording": {
|
|
**recording,
|
|
"active_epoch": None,
|
|
"producer_alive": False,
|
|
},
|
|
}
|
|
)
|
|
terminal = service.state()
|
|
assert terminal["acquisition"]["state"] == "failed"
|
|
assert terminal["acquisition"]["result"]["camera_failure_code"] == (
|
|
"camera-storage-failed"
|
|
)
|
|
|
|
|
|
def _install_composite_active_recovery_fixture(
|
|
service: XgridsK1CompatibilityService,
|
|
runtime: FakeVisualizationRuntime,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
*,
|
|
runtime_phase: str = "reconnecting",
|
|
camera_phase: str = "error",
|
|
) -> tuple[FakeInteractiveControlSession, dict[str, object]]:
|
|
control = FakeInteractiveControlSession()
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
binding = _seed_supervised_connection(service)
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="RECOVERY001",
|
|
host=binding.target_ipv4,
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = str(prepared["acquisition"]["acquisition_id"])
|
|
service.start_acquisition(
|
|
_start_request(
|
|
acquisition_id=acquisition_id,
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
)
|
|
with service._lock: # noqa: SLF001
|
|
acquisition = service._acquisition # noqa: SLF001
|
|
start_operation_id = service._acquisition_start_operation_id # noqa: SLF001
|
|
out_dir = service._acquisition_out_dir # noqa: SLF001
|
|
assert acquisition is not None
|
|
assert start_operation_id is not None
|
|
assert out_dir is not None
|
|
acquisition.transition("acquiring", message_code="acquisition.acquiring")
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
runtime.phase = runtime_phase
|
|
runtime.source_mode = "live"
|
|
runtime.source_ready = runtime_phase == "live"
|
|
runtime.recovery_state = (
|
|
"reconnecting" if runtime_phase == "reconnecting" else "inactive"
|
|
)
|
|
runtime.recovery_request_pending = runtime_phase == "reconnecting"
|
|
runtime.pcl_frames = 4
|
|
|
|
physical: dict[str, object] = {
|
|
"status": "resolved",
|
|
"reason_code": None,
|
|
"requires_reconciliation": False,
|
|
"resolved_active_recovery_required": True,
|
|
"automatic_replay_allowed": False,
|
|
"normal_session_recovery_supported": False,
|
|
"recovery_requirement": ("explicit-read-only-deviceinfo-and-non-retained-devicestatus"),
|
|
"runtime_bound": True,
|
|
"reconciliation_ready": False,
|
|
"observed_session_state": None,
|
|
"active_operation_id": None,
|
|
"record": {
|
|
"revision": 7,
|
|
"operation_id": start_operation_id,
|
|
"acquisition_id": acquisition_id,
|
|
"action": "start",
|
|
"stage": "resolved",
|
|
"resolution": "start-active-observed",
|
|
"publish_call_returned": True,
|
|
"qos2_completed": True,
|
|
"packet_id": 41,
|
|
"application_response": {
|
|
"operation_id": start_operation_id,
|
|
"action": "start",
|
|
"success": True,
|
|
},
|
|
"last_status": {
|
|
"session_state": "scanning",
|
|
"project_bound": True,
|
|
"init_ready": True,
|
|
"mqtt_retained": False,
|
|
},
|
|
"connection": {
|
|
"intent_id": binding.intent_id,
|
|
"transport_ref": binding.transport_ref,
|
|
"connection_mode": binding.connection_mode,
|
|
"target_ipv4": binding.target_ipv4,
|
|
"target_port": binding.target_port,
|
|
},
|
|
"reconciliations": [],
|
|
},
|
|
}
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"snapshot",
|
|
lambda: physical,
|
|
)
|
|
# START operation ownership is released after the normal application
|
|
# operation settles. The broad mapping-only fixture has no real durable
|
|
# checkpoint, so preserve its immutable original START through the same
|
|
# helper seam used by production checkpoint integration.
|
|
service._active_acquisition_checkpoint_start_operation_id = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
lambda _service, **_kwargs: start_operation_id,
|
|
service,
|
|
)
|
|
camera = service.camera_preview.snapshot()
|
|
camera.update(
|
|
{
|
|
"phase": camera_phase,
|
|
"active_source_id": facade_module.DEFAULT_ACQUISITION_CAMERA_SOURCE,
|
|
"recording": {
|
|
"active": True,
|
|
"session": out_dir.name,
|
|
"active_epoch": 1 if camera_phase == "streaming" else None,
|
|
"producer_alive": camera_phase == "streaming",
|
|
"producer_age_ms": 1_000,
|
|
"last_segment_age_ms": 0 if camera_phase == "streaming" else None,
|
|
"completed_epochs": 0 if camera_phase == "streaming" else 1,
|
|
"last_summary": None,
|
|
"source_end_expected": False,
|
|
},
|
|
"error": (
|
|
{
|
|
"code": "camera-source-ended",
|
|
"message": "synthetic Wi-Fi loss",
|
|
}
|
|
if camera_phase == "error"
|
|
else None
|
|
),
|
|
}
|
|
)
|
|
monkeypatch.setattr(service.camera_preview, "snapshot", lambda: camera)
|
|
return control, physical
|
|
|
|
|
|
def test_camera_first_short_outage_is_observational_without_browser_side_effects(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
runtime_phase="live",
|
|
camera_phase="error",
|
|
)
|
|
start_projects_before = list(control.start_projects)
|
|
stop_calls_before = control.stop_calls
|
|
|
|
state = service.state()
|
|
|
|
recovery = state["connection_recovery"]
|
|
assert runtime.recovery_requests == []
|
|
assert runtime.recovery_request_pending is False
|
|
assert state["source_mode"] == "live"
|
|
assert recovery["state"] == "inactive"
|
|
assert recovery["automatic_read_only_rebind"] is False
|
|
assert state["acquisition"]["state"] == "acquiring"
|
|
assert state["selected_device_id"] == "test-ble-transport"
|
|
assert state["k1_ip"] == "192.168.1.20"
|
|
assert runtime.stop_calls == 0
|
|
assert control.start_projects == start_projects_before
|
|
assert control.stop_calls == stop_calls_before
|
|
assert service._physical_command_coordinator.snapshot() is physical # noqa: SLF001
|
|
|
|
|
|
def test_camera_stall_snapshot_does_not_reconnect_live_mqtt_runtime(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
runtime_phase="live",
|
|
camera_phase="streaming",
|
|
)
|
|
camera = service.camera_preview.snapshot()
|
|
recording = camera["recording"]
|
|
assert isinstance(recording, dict)
|
|
recording["last_segment_age_ms"] = (
|
|
facade_module.ACTIVE_STREAM_CAMERA_STALL_MILLISECONDS
|
|
)
|
|
|
|
state = service.state()
|
|
|
|
assert runtime.recovery_requests == []
|
|
assert runtime.recovery_request_pending is False
|
|
assert state["source_mode"] == "live"
|
|
assert state["acquisition"]["state"] == "acquiring"
|
|
assert runtime.stop_calls == 0
|
|
assert control.stop_calls == 0
|
|
assert service._physical_command_coordinator.snapshot() is physical # noqa: SLF001
|
|
|
|
|
|
def test_backend_camera_watchdog_cas_restarts_only_camera_without_device_commands(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
runtime_phase="live",
|
|
camera_phase="streaming",
|
|
)
|
|
camera = service.camera_preview.snapshot()
|
|
camera["generation"] = 1
|
|
recording = camera["recording"]
|
|
assert isinstance(recording, dict)
|
|
recording.update(
|
|
{
|
|
"producer_age_ms": facade_module.ACTIVE_STREAM_CAMERA_STALL_MILLISECONDS + 1,
|
|
"last_segment_age_ms": facade_module.ACTIVE_STREAM_CAMERA_STALL_MILLISECONDS + 1,
|
|
"last_media_segment_age_ms": (
|
|
facade_module.ACTIVE_STREAM_CAMERA_STALL_MILLISECONDS + 1
|
|
),
|
|
"committed_media_segment_count": 10,
|
|
"media_ready": True,
|
|
"current_epoch": {
|
|
"generation": 1,
|
|
"init_committed": True,
|
|
"init_committed_age_ms": 20_000,
|
|
"first_media_committed": True,
|
|
"first_media_committed_age_ms": 20_000,
|
|
"committed_media_segment_count": 10,
|
|
"last_media_segment_age_ms": (
|
|
facade_module.ACTIVE_STREAM_CAMERA_STALL_MILLISECONDS + 1
|
|
),
|
|
},
|
|
}
|
|
)
|
|
restart_calls: list[tuple[str, str, int, str, int | None, int]] = []
|
|
|
|
def restart(
|
|
source_id: str,
|
|
target: str,
|
|
*,
|
|
expected_generation: int,
|
|
expected_recording_session: str,
|
|
expected_active_epoch: int | None,
|
|
expected_recording_media_segment_count: int,
|
|
pre_detach_fence: Callable[[Callable[[], bool]], bool],
|
|
commit_fence: Callable[[Callable[[], bool]], bool] | None = None,
|
|
committed_before_start: Callable[[dict[str, object]], None] | None = None,
|
|
) -> dict[str, object]:
|
|
restart_calls.append(
|
|
(
|
|
source_id,
|
|
target,
|
|
expected_generation,
|
|
expected_recording_session,
|
|
expected_active_epoch,
|
|
expected_recording_media_segment_count,
|
|
)
|
|
)
|
|
assert pre_detach_fence(lambda: True) is True
|
|
camera.update(
|
|
{
|
|
"phase": "connecting",
|
|
"generation": 2,
|
|
"error": None,
|
|
"recording": {
|
|
**recording,
|
|
"active_epoch": 2,
|
|
"producer_alive": True,
|
|
"producer_age_ms": 0,
|
|
"last_segment_age_ms": None,
|
|
"last_media_segment_age_ms": None,
|
|
"media_ready": False,
|
|
"current_epoch": {
|
|
"generation": 2,
|
|
"init_committed": False,
|
|
"init_committed_age_ms": None,
|
|
"first_media_committed": False,
|
|
"first_media_committed_age_ms": None,
|
|
"committed_media_segment_count": 0,
|
|
"last_media_segment_age_ms": None,
|
|
},
|
|
},
|
|
}
|
|
)
|
|
assert commit_fence is not None
|
|
assert committed_before_start is not None
|
|
assert commit_fence(
|
|
lambda: committed_before_start(dict(camera)) is None
|
|
) is True
|
|
return dict(camera)
|
|
|
|
monkeypatch.setattr(service.camera_preview, "restart_recording_producer", restart)
|
|
start_projects_before = list(control.start_projects)
|
|
stop_calls_before = control.stop_calls
|
|
|
|
service._observe_camera_producer_stall("camera-stream-stalled", 1) # noqa: SLF001
|
|
service._observe_camera_producer_stall("camera-stream-stalled", 1) # noqa: SLF001
|
|
|
|
assert restart_calls == [
|
|
(
|
|
facade_module.DEFAULT_ACQUISITION_CAMERA_SOURCE,
|
|
"192.168.1.20",
|
|
1,
|
|
recording["session"],
|
|
1,
|
|
10,
|
|
)
|
|
]
|
|
assert runtime.phase == "live"
|
|
assert runtime.recovery_requests == []
|
|
assert control.start_projects == start_projects_before
|
|
assert control.stop_calls == stop_calls_before
|
|
assert service._physical_command_coordinator.snapshot() is physical # noqa: SLF001
|
|
assert camera["recording"]["media_ready"] is False
|
|
projected = service.state()
|
|
assert projected["connection_recovery"]["state"] == "inactive"
|
|
assert projected["connection_recovery"]["camera_recovery"] == "owned"
|
|
assert projected["connection_recovery"]["camera_media_state"] == "pending-init"
|
|
assert projected["connection_recovery"]["camera_media_ready"] is False
|
|
assert projected["connection_recovery"]["camera_epoch"]["generation"] == 2
|
|
assert projected["connection_policy"]["actions"]["stop-acquisition"]["allowed"] is True
|
|
|
|
|
|
def test_stop_priority_overtakes_blocked_camera_watchdog_candidate(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, _physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
runtime_phase="live",
|
|
camera_phase="streaming",
|
|
)
|
|
camera = service.camera_preview.snapshot()
|
|
camera["generation"] = 1
|
|
recording = camera["recording"]
|
|
assert isinstance(recording, dict)
|
|
recording.update(
|
|
{
|
|
"producer_age_ms": facade_module.ACTIVE_STREAM_CAMERA_STALL_MILLISECONDS + 1,
|
|
"last_segment_age_ms": facade_module.ACTIVE_STREAM_CAMERA_STALL_MILLISECONDS + 1,
|
|
"last_media_segment_age_ms": (
|
|
facade_module.ACTIVE_STREAM_CAMERA_STALL_MILLISECONDS + 1
|
|
),
|
|
"committed_media_segment_count": 10,
|
|
"media_ready": True,
|
|
"current_epoch": {
|
|
"generation": 1,
|
|
"init_committed": True,
|
|
"init_committed_age_ms": 20_000,
|
|
"first_media_committed": True,
|
|
"first_media_committed_age_ms": 20_000,
|
|
"committed_media_segment_count": 10,
|
|
"last_media_segment_age_ms": 20_000,
|
|
},
|
|
}
|
|
)
|
|
candidate_blocked = threading.Event()
|
|
release_candidate = threading.Event()
|
|
candidate_committed: list[bool] = []
|
|
|
|
def restart(
|
|
_source_id: str,
|
|
_target: str,
|
|
*,
|
|
expected_generation: int,
|
|
expected_recording_session: str,
|
|
expected_active_epoch: int | None,
|
|
expected_recording_media_segment_count: int,
|
|
pre_detach_fence: Callable[[Callable[[], bool]], bool],
|
|
commit_fence: Callable[[Callable[[], bool]], bool] | None = None,
|
|
committed_before_start: Callable[[dict[str, object]], None] | None = None,
|
|
) -> dict[str, object]:
|
|
del (
|
|
expected_generation,
|
|
expected_recording_session,
|
|
expected_active_epoch,
|
|
expected_recording_media_segment_count,
|
|
)
|
|
assert commit_fence is not None
|
|
del committed_before_start
|
|
candidate_blocked.set()
|
|
assert release_candidate.wait(3.0)
|
|
committed = commit_fence(lambda: candidate_committed.append(True) or True)
|
|
if not committed:
|
|
raise ValueError("camera recovery lineage устарела")
|
|
return dict(camera)
|
|
|
|
monkeypatch.setattr(service.camera_preview, "restart_recording_producer", restart)
|
|
stop_owned_entered = threading.Event()
|
|
|
|
def stop_owned(
|
|
_request: StopAcquisitionRequest,
|
|
_preadmitted_stop: object | None = None,
|
|
) -> dict[str, object]:
|
|
stop_owned_entered.set()
|
|
control.request_stop(
|
|
confirmation=object(),
|
|
command_context=object(),
|
|
dispatch_admission_deadline_reached=lambda: False,
|
|
expected_session_generation=control.session_generation,
|
|
expected_state_revision=control.state_revision,
|
|
)
|
|
with service._lock: # noqa: SLF001
|
|
acquisition = service._acquisition # noqa: SLF001
|
|
assert acquisition is not None
|
|
acquisition.transition(
|
|
"awaiting_external_stop",
|
|
message_code="acquisition.stop.device_stopping",
|
|
)
|
|
service._acquisition_stop_operation_id = "priority-stop" # noqa: SLF001
|
|
return {"stop": "prepared"}
|
|
|
|
monkeypatch.setattr(service, "_stop_acquisition_owned", stop_owned)
|
|
observer = threading.Thread(
|
|
target=service._observe_camera_producer_stall, # noqa: SLF001
|
|
args=("camera-stream-stalled", 1),
|
|
daemon=True,
|
|
)
|
|
observer.start()
|
|
assert candidate_blocked.wait(3.0)
|
|
|
|
request = _stop_request(
|
|
acquisition_id=None,
|
|
mode="graceful",
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
stop_result: list[dict[str, object]] = []
|
|
stop_worker = threading.Thread(
|
|
target=lambda: stop_result.append(service.stop_acquisition(request)),
|
|
daemon=True,
|
|
)
|
|
stop_worker.start()
|
|
assert stop_owned_entered.wait(3.0)
|
|
stop_worker.join(3.0)
|
|
|
|
assert stop_worker.is_alive() is False
|
|
assert stop_result == [{"stop": "prepared"}]
|
|
assert control.stop_calls == 1
|
|
assert candidate_committed == []
|
|
|
|
release_candidate.set()
|
|
observer.join(3.0)
|
|
assert observer.is_alive() is False
|
|
assert candidate_committed == []
|
|
|
|
|
|
def test_stop_priority_overtakes_blocked_physical_snapshot_before_camera_commit(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
_control, physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
runtime_phase="live",
|
|
camera_phase="streaming",
|
|
)
|
|
with service._lock: # noqa: SLF001
|
|
acquisition = service._acquisition # noqa: SLF001
|
|
out_dir = service._acquisition_out_dir # noqa: SLF001
|
|
start_operation_id = service._acquisition_start_operation_id # noqa: SLF001
|
|
assert acquisition is not None
|
|
assert out_dir is not None
|
|
assert start_operation_id is not None
|
|
|
|
snapshot_blocked = threading.Event()
|
|
release_snapshot = threading.Event()
|
|
|
|
def blocking_snapshot() -> dict[str, object]:
|
|
if threading.current_thread().name == "camera-commit-candidate":
|
|
snapshot_blocked.set()
|
|
assert release_snapshot.wait(3.0)
|
|
return physical
|
|
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"snapshot",
|
|
blocking_snapshot,
|
|
)
|
|
camera_commit_calls: list[str] = []
|
|
camera_commit_results: list[bool] = []
|
|
camera_commit_worker = threading.Thread(
|
|
name="camera-commit-candidate",
|
|
target=lambda: camera_commit_results.append(
|
|
service._commit_camera_restart_if_still_active( # noqa: SLF001
|
|
acquisition_id=acquisition.acquisition_id,
|
|
evidence_session_id=out_dir.name,
|
|
start_operation_id=start_operation_id,
|
|
runtime_producer_generation=runtime.producer_generation,
|
|
commit=lambda: camera_commit_calls.append("commit") or True,
|
|
)
|
|
),
|
|
daemon=True,
|
|
)
|
|
camera_commit_worker.start()
|
|
assert snapshot_blocked.wait(3.0)
|
|
|
|
stop_owned_entered = threading.Event()
|
|
|
|
def stop_owned(
|
|
_request: StopAcquisitionRequest,
|
|
_preadmitted_stop: object | None = None,
|
|
) -> dict[str, object]:
|
|
assert service._camera_stop_priority_counts == { # noqa: SLF001
|
|
acquisition.acquisition_id: 1
|
|
}
|
|
stop_owned_entered.set()
|
|
with service._lock: # noqa: SLF001
|
|
acquisition.transition(
|
|
"awaiting_external_stop",
|
|
message_code="acquisition.stop.device_stopping",
|
|
)
|
|
service._acquisition_stop_operation_id = "snapshot-priority-stop" # noqa: SLF001
|
|
return {"stop": "prepared"}
|
|
|
|
monkeypatch.setattr(service, "_stop_acquisition_owned", stop_owned)
|
|
stop_result: list[dict[str, object]] = []
|
|
stop_worker = threading.Thread(
|
|
target=lambda: stop_result.append(
|
|
service.stop_acquisition(
|
|
_stop_request(
|
|
acquisition_id=None,
|
|
mode="graceful",
|
|
operator_confirmed=True,
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
)
|
|
),
|
|
daemon=True,
|
|
)
|
|
stop_worker.start()
|
|
assert stop_owned_entered.wait(3.0)
|
|
stop_worker.join(3.0)
|
|
assert stop_worker.is_alive() is False
|
|
assert stop_result == [{"stop": "prepared"}]
|
|
|
|
release_snapshot.set()
|
|
camera_commit_worker.join(3.0)
|
|
assert camera_commit_worker.is_alive() is False
|
|
assert camera_commit_results == [False]
|
|
assert camera_commit_calls == []
|
|
|
|
|
|
def test_stop_priority_overtakes_blocked_initial_camera_popen(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
with service._lock: # noqa: SLF001
|
|
acquisition = service._acquisition # noqa: SLF001
|
|
out_dir = service._acquisition_out_dir # noqa: SLF001
|
|
assert acquisition is not None
|
|
assert out_dir is not None
|
|
camera = service.camera_preview.snapshot()
|
|
camera.update(
|
|
{
|
|
"phase": "idle",
|
|
"generation": None,
|
|
"active_source_id": None,
|
|
"recording": {"active": False},
|
|
"error": None,
|
|
}
|
|
)
|
|
candidate_blocked = threading.Event()
|
|
release_candidate = threading.Event()
|
|
candidate_committed: list[bool] = []
|
|
|
|
def blocked_initial_popen(
|
|
_source_id: str,
|
|
_target: str,
|
|
_session_dir: Path,
|
|
*,
|
|
commit_fence: Callable[[Callable[[], bool]], bool],
|
|
committed_before_start: Callable[[dict[str, object]], None] | None = None,
|
|
) -> dict[str, object]:
|
|
del committed_before_start
|
|
candidate_blocked.set()
|
|
assert release_candidate.wait(3.0)
|
|
if not commit_fence(lambda: candidate_committed.append(True) or True):
|
|
raise ValueError("camera activation lineage устарела")
|
|
return dict(camera)
|
|
|
|
monkeypatch.setattr(
|
|
service.camera_preview,
|
|
"activate_recording_producer",
|
|
blocked_initial_popen,
|
|
)
|
|
monkeypatch.setattr(service, "_ensure_camera_preview_process_lease", lambda: False)
|
|
activation_result: list[bool] = []
|
|
activation_errors: list[BaseException] = []
|
|
|
|
def activate() -> None:
|
|
try:
|
|
activation_result.append(
|
|
service._activate_default_acquisition_camera( # noqa: SLF001
|
|
expected_acquisition_id=acquisition.acquisition_id,
|
|
expected_evidence_session_id=out_dir.name,
|
|
expected_runtime_generation=runtime.producer_generation,
|
|
)
|
|
)
|
|
except BaseException as exc:
|
|
activation_errors.append(exc)
|
|
|
|
activation = threading.Thread(
|
|
target=activate,
|
|
daemon=True,
|
|
)
|
|
activation.start()
|
|
assert candidate_blocked.wait(3.0)
|
|
|
|
stop_result: list[dict[str, object]] = []
|
|
stop_worker = threading.Thread(
|
|
target=lambda: stop_result.append(
|
|
service.stop_acquisition(
|
|
fixture.request.model_copy(update={"acquisition_id": None})
|
|
)
|
|
),
|
|
daemon=True,
|
|
)
|
|
stop_worker.start()
|
|
stop_worker.join(3.0)
|
|
assert stop_worker.is_alive() is False
|
|
assert len(stop_result) == 1
|
|
assert stop_result[0]["acquisition"]["state"] == "awaiting_external_stop"
|
|
assert fixture.control.stop_calls == 1
|
|
assert fixture.prepare_calls == [fixture.stop_operation_id]
|
|
prepared_record = fixture.ledger.snapshot().record
|
|
assert prepared_record is not None
|
|
assert prepared_record.operation_id == fixture.stop_operation_id
|
|
assert prepared_record.stage == "prepared"
|
|
|
|
release_candidate.set()
|
|
activation.join(3.0)
|
|
assert activation.is_alive() is False
|
|
assert activation_result == []
|
|
assert len(activation_errors) == 1
|
|
assert isinstance(activation_errors[0], ValueError)
|
|
assert candidate_committed == []
|
|
|
|
|
|
def test_nontransport_camera_failure_remains_terminal_without_recovery_wake(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
runtime_phase="live",
|
|
camera_phase="streaming",
|
|
)
|
|
camera = service.camera_preview.snapshot()
|
|
camera["phase"] = "error"
|
|
camera["error"] = {
|
|
"code": "camera-artifact-write-failed",
|
|
"message": "synthetic local storage failure",
|
|
}
|
|
monkeypatch.setattr(service, "_seal_acquisition_capture_clock", lambda: None)
|
|
|
|
state = service.state()
|
|
|
|
assert runtime.recovery_requests == []
|
|
assert runtime.recovery_request_pending is False
|
|
assert state["acquisition"]["state"] == "failed"
|
|
assert state["acquisition"]["result"]["camera_failure_code"] == (
|
|
"camera-artifact-write-failed"
|
|
)
|
|
assert runtime.stop_calls == 1
|
|
assert control.stop_calls == 0
|
|
assert service._physical_command_coordinator.snapshot() is physical # noqa: SLF001
|
|
|
|
|
|
def test_host_first_recovery_does_not_mask_later_camera_artifact_failure(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
runtime_phase="live",
|
|
camera_phase="streaming",
|
|
)
|
|
monkeypatch.setattr(service, "_seal_acquisition_capture_clock", lambda: None)
|
|
lost_path = HostPathProbeResult(
|
|
available=False,
|
|
fingerprint=None,
|
|
interface=None,
|
|
source_ipv4=None,
|
|
route_class="unavailable",
|
|
reason_code="host-route-unavailable",
|
|
)
|
|
start_projects_before = list(control.start_projects)
|
|
stop_calls_before = control.stop_calls
|
|
|
|
for _ in range(3):
|
|
service._connection_supervisor.observe_host_path(lost_path) # noqa: SLF001
|
|
reconnecting = service.state()
|
|
assert reconnecting["phase"] == "reconnecting"
|
|
assert runtime.recovery_request_pending is True
|
|
|
|
camera = service.camera_preview.snapshot()
|
|
camera["phase"] = "error"
|
|
camera["error"] = {
|
|
"code": "camera-artifact-write-failed",
|
|
"message": "synthetic local artifact failure after host loss",
|
|
}
|
|
|
|
failed = service.state()
|
|
|
|
assert failed["acquisition"]["state"] == "failed"
|
|
assert failed["acquisition"]["result"]["camera_failure_code"] == (
|
|
"camera-artifact-write-failed"
|
|
)
|
|
assert failed["source_mode"] == "idle"
|
|
assert runtime.stop_calls == 1
|
|
assert runtime.recovery_request_pending is False
|
|
assert control.start_projects == start_projects_before
|
|
assert control.stop_calls == stop_calls_before
|
|
assert service._physical_command_coordinator.snapshot() is physical # noqa: SLF001
|
|
|
|
|
|
def test_control_first_loss_freezes_topology_before_ephemeral_retirement(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
runtime_phase="live",
|
|
camera_phase="streaming",
|
|
)
|
|
control.state = "failed"
|
|
control.failure = {
|
|
"reason_code": "mqtt_network_loop_failed",
|
|
"modeling_command_attempted": True,
|
|
"safe_to_retry": False,
|
|
}
|
|
start_projects_before = list(control.start_projects)
|
|
stop_calls_before = control.stop_calls
|
|
|
|
state = service.state()
|
|
|
|
assert runtime.recovery_requests == [
|
|
("mqtt_network_loop_failed", runtime.producer_generation),
|
|
]
|
|
assert state["phase"] == "reconnecting"
|
|
assert state["connection_recovery"]["automatic_read_only_rebind"] is True
|
|
assert state["acquisition"]["state"] == "acquiring"
|
|
assert state["selected_device_id"] == "test-ble-transport"
|
|
assert state["device_session"] is not None
|
|
assert state["k1_ip"] == "192.168.1.20"
|
|
assert runtime.stop_calls == 0
|
|
assert control.start_projects == start_projects_before
|
|
assert control.stop_calls == stop_calls_before
|
|
assert service._physical_command_coordinator.snapshot() is physical # noqa: SLF001
|
|
|
|
|
|
def test_incident_host_route_streak_wakes_recovery_before_binding_retirement(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, _physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
runtime_phase="live",
|
|
camera_phase="streaming",
|
|
)
|
|
lost_path = HostPathProbeResult(
|
|
available=False,
|
|
fingerprint=None,
|
|
interface=None,
|
|
source_ipv4=None,
|
|
route_class="unavailable",
|
|
reason_code="host-route-unavailable",
|
|
)
|
|
|
|
states: list[dict[str, Any]] = []
|
|
for _ in range(3):
|
|
service._connection_supervisor.observe_host_path(lost_path) # noqa: SLF001
|
|
states.append(service.state())
|
|
|
|
assert runtime.recovery_requests == [
|
|
("host-route-unavailable", runtime.producer_generation),
|
|
]
|
|
assert all(state["selected_device_id"] == "test-ble-transport" for state in states)
|
|
assert states[-1]["phase"] == "reconnecting"
|
|
assert states[-1]["connection_recovery"]["automatic_read_only_rebind"] is True
|
|
assert states[-1]["acquisition"]["state"] == "acquiring"
|
|
assert control.state == "scanning"
|
|
assert runtime.stop_calls == 0
|
|
|
|
|
|
@pytest.mark.parametrize("first_symptom", ["camera", "control"])
|
|
def test_host_camera_and_late_mqtt_loss_share_one_recovery_owner(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
first_symptom: str,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, _physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
runtime_phase="live",
|
|
camera_phase="error" if first_symptom == "camera" else "streaming",
|
|
)
|
|
|
|
if first_symptom == "camera":
|
|
first = service.state()
|
|
assert first["connection_recovery"]["state"] == "inactive"
|
|
assert runtime.recovery_requests == []
|
|
control.state = "failed"
|
|
control.failure = {
|
|
"reason_code": "mqtt_network_loop_failed",
|
|
"modeling_command_attempted": True,
|
|
"safe_to_retry": False,
|
|
}
|
|
else:
|
|
control.state = "failed"
|
|
control.failure = {
|
|
"reason_code": "mqtt_network_loop_failed",
|
|
"modeling_command_attempted": True,
|
|
"safe_to_retry": False,
|
|
}
|
|
first = service.state()
|
|
camera = service.camera_preview.snapshot()
|
|
camera["phase"] = "error"
|
|
camera["error"] = {
|
|
"code": "camera-source-ended",
|
|
"message": "synthetic later camera loss",
|
|
}
|
|
second = service.state()
|
|
generation = second["connection_recovery"]["generation"]
|
|
# A later Paho callback sees the already reconnecting runtime and therefore
|
|
# joins this owner; it cannot queue a second capture wake or lineage.
|
|
assert service._active_stream_recovery_admitted_for_runtime_loss() is True # noqa: SLF001
|
|
|
|
assert len(runtime.recovery_requests) == 1
|
|
assert second["connection_recovery"]["generation"] == generation
|
|
assert second["connection_recovery"]["state"] == "reconnecting"
|
|
assert second["acquisition"]["state"] == "acquiring"
|
|
assert runtime.stop_calls == 0
|
|
|
|
|
|
def test_active_stream_recovery_survives_old_control_keepalive_failure(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, _physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
)
|
|
|
|
admitted = service.state()
|
|
assert admitted["connection_recovery"]["state"] == "reconnecting"
|
|
with service._lock: # noqa: SLF001
|
|
assert service._active_stream_recovery_started_monotonic is not None # noqa: SLF001
|
|
service._active_stream_recovery_started_monotonic -= 60.0 # noqa: SLF001
|
|
control.state = "failed"
|
|
control.failure = {
|
|
"reason_code": "mqtt_network_loop_failed",
|
|
"modeling_command_attempted": True,
|
|
"safe_to_retry": False,
|
|
}
|
|
|
|
after_keepalive = service.state()
|
|
|
|
assert after_keepalive["phase"] == "reconnecting"
|
|
assert after_keepalive["connection_recovery"]["elapsed_ms"] >= 60_000
|
|
assert after_keepalive["connection_recovery"]["automatic_command_retry"] is False
|
|
assert after_keepalive["acquisition"]["state"] == "acquiring"
|
|
assert after_keepalive["selected_device_id"] == "test-ble-transport"
|
|
assert after_keepalive["k1_ip"] == "192.168.1.20"
|
|
assert runtime.stop_calls == 0
|
|
|
|
|
|
def test_recovered_projection_requires_a_live_ready_source_and_terminal_lease_retires_owner(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
_control, _physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
)
|
|
reconnecting = service.state()
|
|
lineage = service._active_stream_recovery_lineage # noqa: SLF001
|
|
assert lineage is not None
|
|
assert reconnecting["connection_recovery"]["state"] == "reconnecting"
|
|
|
|
runtime.phase = "idle"
|
|
runtime.source_mode = "idle"
|
|
runtime.source_ready = False
|
|
projected_reconnecting_idle = service._active_stream_recovery_projection( # noqa: SLF001
|
|
runtime.snapshot()
|
|
)
|
|
assert projected_reconnecting_idle["state"] == "inactive"
|
|
assert projected_reconnecting_idle["automatic_read_only_rebind"] is False
|
|
assert projected_reconnecting_idle["acquisition_id"] is None
|
|
|
|
runtime.phase = "reconnecting"
|
|
runtime.source_mode = "live"
|
|
|
|
runtime.phase = "live"
|
|
runtime.source_ready = True
|
|
runtime.recovery_state = "recovered"
|
|
runtime.recovery_attempt = 7
|
|
recovered = service._active_stream_recovery_projection(runtime.snapshot()) # noqa: SLF001
|
|
|
|
assert recovered["state"] == "recovered"
|
|
assert recovered["attempt"] == 7
|
|
assert recovered["automatic_read_only_rebind"] is True
|
|
|
|
# An out-of-order runtime tail cannot publish recovered while the receiver
|
|
# is terminal. Durable physical-command history is owned by its ledger, not
|
|
# by this process-local recovery contract.
|
|
runtime.phase = "idle"
|
|
runtime.source_mode = "idle"
|
|
runtime.source_ready = False
|
|
projected_idle = service._active_stream_recovery_projection(runtime.snapshot()) # noqa: SLF001
|
|
assert projected_idle["state"] == "inactive"
|
|
assert projected_idle["automatic_read_only_rebind"] is False
|
|
assert projected_idle["acquisition_id"] is None
|
|
assert projected_idle["camera_recovery"] == "inactive"
|
|
|
|
service._release_acquisition_session_lease() # noqa: SLF001
|
|
retired = service._active_stream_recovery_projection(runtime.snapshot()) # noqa: SLF001
|
|
assert retired["state"] == "inactive"
|
|
assert retired["automatic_read_only_rebind"] is False
|
|
assert retired["acquisition_id"] is None
|
|
assert service._active_stream_recovery_lineage is None # noqa: SLF001
|
|
|
|
|
|
def test_local_force_finish_cancels_generation_and_is_idempotent_for_mode_reset(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
_control, physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
runtime_phase="reconnecting",
|
|
camera_phase="error",
|
|
)
|
|
monkeypatch.setattr(service, "_seal_acquisition_capture_clock", lambda: None)
|
|
state = service.state()
|
|
acquisition = state["acquisition"]
|
|
recovery = state["connection_recovery"]
|
|
lineage = service._active_stream_recovery_lineage # noqa: SLF001
|
|
assert lineage is not None
|
|
assert runtime.recovery_request_pending is True
|
|
|
|
finished = service.force_finish_acquisition_locally(
|
|
_force_finish_request(
|
|
acquisition_id=acquisition["acquisition_id"],
|
|
expected_state_revision=acquisition["state_revision"],
|
|
expected_recovery_generation=recovery["generation"],
|
|
)
|
|
)
|
|
|
|
assert finished["acquisition"]["state"] == "interrupted"
|
|
assert finished["acquisition"]["result"]["device_stop"] == "not-sent"
|
|
assert finished["acquisition"]["result"]["physical_command_sent"] is False
|
|
assert finished["connection_recovery"]["state"] == "force-finished"
|
|
assert runtime.stop_calls == 1
|
|
assert runtime.recovery_request_pending is False
|
|
assert service._active_stream_recovery_lineage_is_current(lineage) is False # noqa: SLF001
|
|
assert service._physical_command_coordinator.snapshot() is physical # noqa: SLF001
|
|
with service._acquisition_lifecycle_gate: # noqa: SLF001
|
|
assert (
|
|
service._force_finish_active_acquisition_locally( # noqa: SLF001
|
|
reason_code="connection-mode-reset",
|
|
require_recovery=False,
|
|
)
|
|
is False
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("cleanup_failure", ["runtime", "camera"])
|
|
def test_local_force_finish_cleanup_failure_is_visible_retryable_and_fences_late_success(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
cleanup_failure: str,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
)
|
|
monkeypatch.setattr(service, "_seal_acquisition_capture_clock", lambda: None)
|
|
before = service.state()
|
|
acquisition = before["acquisition"]
|
|
recovery = before["connection_recovery"]
|
|
lineage = service._active_stream_recovery_lineage # noqa: SLF001
|
|
assert lineage is not None
|
|
start_projects_before = list(control.start_projects)
|
|
stop_calls_before = control.stop_calls
|
|
|
|
original_stop_recording = service.camera_preview.stop_recording
|
|
camera_failure_active = cleanup_failure == "camera"
|
|
|
|
def stop_recording(**kwargs: object) -> dict[str, object]:
|
|
nonlocal camera_failure_active
|
|
if camera_failure_active:
|
|
camera_failure_active = False
|
|
raise RuntimeError("synthetic camera local cleanup failure")
|
|
return original_stop_recording(**kwargs) # type: ignore[arg-type,return-value]
|
|
|
|
if cleanup_failure == "runtime":
|
|
runtime.stop_error = RuntimeError("synthetic runtime local cleanup failure")
|
|
else:
|
|
monkeypatch.setattr(service.camera_preview, "stop_recording", stop_recording)
|
|
|
|
with pytest.raises(RuntimeError, match=f"synthetic {cleanup_failure} local cleanup failure"):
|
|
service.force_finish_acquisition_locally(
|
|
_force_finish_request(
|
|
acquisition_id=acquisition["acquisition_id"],
|
|
expected_state_revision=acquisition["state_revision"],
|
|
expected_recovery_generation=recovery["generation"],
|
|
)
|
|
)
|
|
|
|
failed = service.state()
|
|
failed_operation = failed["last_operation"]
|
|
assert failed["acquisition"]["state"] == "failed"
|
|
assert failed["acquisition"]["cleanup_pending"] is True
|
|
assert failed["acquisition"]["result"]["local_force_finish"] is True
|
|
assert failed["acquisition"]["result"]["device_stop"] == "not-sent"
|
|
assert failed["acquisition"]["result"]["physical_command_sent"] is False
|
|
assert failed_operation["action"] == facade_module.ACTION_ACQUISITION_FORCE_FINISH
|
|
assert failed_operation["status"] == "failed"
|
|
assert failed_operation["stage_code"] == "local-cleanup-failed"
|
|
assert failed_operation["error"]["retryable"] is True
|
|
assert failed_operation["error"]["safe_to_retry"] is True
|
|
assert failed_operation["error"]["side_effect_status"] == "none"
|
|
assert service._active_stream_recovery_lineage_is_current(lineage) is False # noqa: SLF001
|
|
assert control.start_projects == start_projects_before
|
|
assert control.stop_calls == stop_calls_before
|
|
assert service._physical_command_coordinator.snapshot() is physical # noqa: SLF001
|
|
|
|
# The retry is local finalization only. It seals the retained evidence
|
|
# lease through the compatibility stop seam and cannot revive recovery or
|
|
# publish a physical command.
|
|
runtime.stop_error = None
|
|
recovered = service.stop()
|
|
|
|
assert recovered["acquisition"]["state"] == "failed"
|
|
assert recovered["acquisition"]["cleanup_pending"] is False
|
|
assert recovered["source_mode"] == "idle"
|
|
assert service._active_stream_recovery_lineage_is_current(lineage) is False # noqa: SLF001
|
|
assert control.start_projects == start_projects_before
|
|
assert control.stop_calls == stop_calls_before
|
|
assert service._physical_command_coordinator.snapshot() is physical # noqa: SLF001
|
|
|
|
|
|
def test_connection_mode_reset_supersedes_queued_force_finish_without_state_revival(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
_control, physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
runtime_phase="reconnecting",
|
|
camera_phase="error",
|
|
)
|
|
monkeypatch.setattr(service, "_seal_acquisition_capture_clock", lambda: None)
|
|
queued = service.state()
|
|
queued_acquisition = queued["acquisition"]
|
|
queued_recovery = queued["connection_recovery"]
|
|
assert runtime.recovery_request_pending is True
|
|
|
|
with service._acquisition_lifecycle_gate: # noqa: SLF001
|
|
reset_finished = service._force_finish_active_acquisition_locally( # noqa: SLF001
|
|
reason_code="connection-mode-reset",
|
|
require_recovery=False,
|
|
)
|
|
|
|
assert reset_finished is True
|
|
with pytest.raises(
|
|
facade_module.LocalAcquisitionLifecycleError,
|
|
match="acquisition changed",
|
|
):
|
|
service.force_finish_acquisition_locally(
|
|
_force_finish_request(
|
|
acquisition_id=queued_acquisition["acquisition_id"],
|
|
expected_state_revision=queued_acquisition["state_revision"],
|
|
expected_recovery_generation=queued_recovery["generation"],
|
|
)
|
|
)
|
|
after = service.state()
|
|
assert after["acquisition"]["state"] == "interrupted"
|
|
assert after["connection_recovery"]["state"] == "force-finished"
|
|
assert runtime.stop_calls == 1
|
|
assert runtime.recovery_request_pending is False
|
|
assert service._physical_command_coordinator.snapshot() is physical # noqa: SLF001
|
|
|
|
|
|
def test_active_stream_recovery_scanning_resumes_same_physical_lineage_without_commands(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
)
|
|
service.state()
|
|
lineage = service._active_stream_recovery_lineage # noqa: SLF001
|
|
assert lineage is not None
|
|
physical["reconciliation_ready"] = True
|
|
physical["observed_session_state"] = "scanning"
|
|
control.state = "connection-ready"
|
|
start_projects_before = list(control.start_projects)
|
|
stop_calls_before = control.stop_calls
|
|
reconcile_ids: list[str] = []
|
|
|
|
def reconcile_resolved_active(*, reconciliation_id: str) -> dict[str, object]:
|
|
reconcile_ids.append(reconciliation_id)
|
|
record = physical["record"]
|
|
assert isinstance(record, dict)
|
|
record["revision"] = int(record["revision"]) + 1
|
|
return record
|
|
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"reconcile_resolved_active",
|
|
reconcile_resolved_active,
|
|
)
|
|
monkeypatch.setattr(
|
|
control,
|
|
"request_start",
|
|
lambda **_kwargs: pytest.fail("recovery must not repeat START"),
|
|
)
|
|
monkeypatch.setattr(
|
|
control,
|
|
"request_stop",
|
|
lambda **_kwargs: pytest.fail("recovery must not send STOP"),
|
|
)
|
|
|
|
decision = asyncio.run(
|
|
service._reconcile_active_stream_physical_state_owned( # noqa: SLF001
|
|
lineage,
|
|
attempt=3,
|
|
)
|
|
)
|
|
|
|
assert decision == "resume"
|
|
assert reconcile_ids == [
|
|
facade_module._active_stream_reconciliation_id( # noqa: SLF001
|
|
lineage,
|
|
attempt=3,
|
|
)
|
|
]
|
|
assert control.state == "scanning"
|
|
assert control.start_projects == start_projects_before
|
|
assert control.stop_calls == stop_calls_before
|
|
# The physical audit revision advanced before MQTT SUBACK. A failed first
|
|
# resubscribe must admit the same frozen lineage on attempt two.
|
|
assert service._admit_active_stream_recovery_lineage() is lineage # noqa: SLF001
|
|
assert (
|
|
physical["record"]
|
|
is service._physical_command_coordinator.snapshot()[ # noqa: SLF001
|
|
"record"
|
|
]
|
|
)
|
|
|
|
|
|
def test_active_stream_recovery_privacy_bridge_retains_exact_route_for_read_only_rebind(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
)
|
|
service.state()
|
|
lineage = service._active_stream_recovery_lineage # noqa: SLF001
|
|
assert lineage is not None
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
baseline = supervisor.snapshot()
|
|
candidate = replace(
|
|
baseline,
|
|
device_identity=replace(baseline.device_identity, state="unverified"),
|
|
control_plane=replace(
|
|
baseline.control_plane,
|
|
state="lost",
|
|
session_id=None,
|
|
),
|
|
lease=replace(baseline.lease, state="configured-unverified"),
|
|
authority=replace(
|
|
baseline.authority,
|
|
control_allowed=False,
|
|
acquisition_start_allowed=False,
|
|
),
|
|
)
|
|
start_projects_before = list(control.start_projects)
|
|
stop_calls_before = control.stop_calls
|
|
operation_journal_before = service._operations.snapshot() # noqa: SLF001
|
|
physical_ledger_before = service._physical_command_ledger.snapshot() # noqa: SLF001
|
|
network_ledger_before = service._network_mutation_ledger.snapshot() # noqa: SLF001
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(
|
|
service._host_wifi_association_probe, # noqa: SLF001
|
|
"observe",
|
|
lambda *_args, **_kwargs: {
|
|
"schema_version": 1,
|
|
"adapter": "CoreWLAN",
|
|
"wifi_interface": True,
|
|
"association_state": "unavailable",
|
|
"evidence_quality": "unavailable",
|
|
"continuity_proven": False,
|
|
"continuity_token": "d" * 64,
|
|
"reason_code": "association-identity-unavailable",
|
|
},
|
|
)
|
|
monkeypatch.setattr(
|
|
supervisor,
|
|
"association_timeout_retention_candidate",
|
|
lambda *, expected_target: (
|
|
candidate
|
|
if expected_target
|
|
== EndpointTarget(lineage.target_ipv4, lineage.target_port)
|
|
else None
|
|
),
|
|
)
|
|
|
|
sampled = service._sample_host_path(lineage.target_ipv4) # noqa: SLF001
|
|
|
|
assert sampled.available is True
|
|
assert sampled.reason_code is None
|
|
assert sampled.fingerprint == baseline.host_path.fingerprint
|
|
assert sampled.kernel_route_fingerprint == (
|
|
baseline.host_path.kernel_route_fingerprint
|
|
)
|
|
assert control.start_projects == start_projects_before
|
|
assert control.stop_calls == stop_calls_before
|
|
assert service._operations.snapshot() == operation_journal_before # noqa: SLF001
|
|
assert ( # noqa: SLF001
|
|
service._physical_command_ledger.snapshot() == physical_ledger_before
|
|
)
|
|
assert service._network_mutation_ledger.snapshot() == network_ledger_before # noqa: SLF001
|
|
assert service._physical_command_coordinator.snapshot() is physical # noqa: SLF001
|
|
|
|
|
|
def test_active_stream_control_adoption_gets_fresh_budget_after_slow_proof(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
)
|
|
service.state()
|
|
lineage = service._active_stream_recovery_lineage # noqa: SLF001
|
|
assert lineage is not None
|
|
physical["reconciliation_ready"] = True
|
|
physical["observed_session_state"] = "scanning"
|
|
control.state = "connection-ready"
|
|
start_projects_before = list(control.start_projects)
|
|
stop_calls_before = control.stop_calls
|
|
original_adopt = control.adopt_reconciled_scanning
|
|
original_snapshot = control.snapshot
|
|
clock = [200.0]
|
|
adoption_pending = [False]
|
|
adoption_snapshots = [0]
|
|
|
|
class FacadeTimeProxy:
|
|
def monotonic(self) -> float:
|
|
return clock[0]
|
|
|
|
def __getattr__(self, name: str) -> Any:
|
|
return getattr(time, name)
|
|
|
|
def reconcile_resolved_active(*, reconciliation_id: str) -> dict[str, object]:
|
|
assert reconciliation_id
|
|
record = physical["record"]
|
|
assert isinstance(record, dict)
|
|
record["revision"] = int(record["revision"]) + 1
|
|
return record
|
|
|
|
def delayed_adopt(**kwargs: object) -> dict[str, object]:
|
|
result = original_adopt(**kwargs)
|
|
adoption_pending[0] = True
|
|
control.state = "active-recovery-requested"
|
|
clock[0] = 205.01
|
|
return result
|
|
|
|
def delayed_snapshot() -> dict[str, object]:
|
|
if adoption_pending[0]:
|
|
adoption_snapshots[0] += 1
|
|
if adoption_snapshots[0] >= 2:
|
|
control.state = "scanning"
|
|
clock[0] = 205.5
|
|
return original_snapshot()
|
|
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"reconcile_resolved_active",
|
|
reconcile_resolved_active,
|
|
)
|
|
monkeypatch.setattr(control, "adopt_reconciled_scanning", delayed_adopt)
|
|
monkeypatch.setattr(control, "snapshot", delayed_snapshot)
|
|
monkeypatch.setattr(facade_module, "time", FacadeTimeProxy())
|
|
monkeypatch.setattr(
|
|
control,
|
|
"request_start",
|
|
lambda **_kwargs: pytest.fail("recovery must not repeat START"),
|
|
)
|
|
monkeypatch.setattr(
|
|
control,
|
|
"request_stop",
|
|
lambda **_kwargs: pytest.fail("recovery must not send STOP"),
|
|
)
|
|
|
|
decision = asyncio.run(
|
|
service._reconcile_active_stream_physical_state_owned( # noqa: SLF001
|
|
lineage,
|
|
attempt=4,
|
|
)
|
|
)
|
|
|
|
assert decision == "resume"
|
|
assert adoption_snapshots[0] >= 2
|
|
assert control.start_projects == start_projects_before
|
|
assert control.stop_calls == stop_calls_before
|
|
|
|
|
|
def test_epoch_one_to_three_rebind_namespaces_prior_acquisition_attempt_collision(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Regress the 2026-08-12 acq-cc9f5310 Wi-Fi recovery failure."""
|
|
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
)
|
|
with service._lock: # noqa: SLF001
|
|
# Match the live process-local counter; another acquisition had
|
|
# already persisted auto-rebind:2:6 in the global ledger history.
|
|
service._active_stream_recovery_generation = 1 # noqa: SLF001
|
|
service.state()
|
|
lineage = service._active_stream_recovery_lineage # noqa: SLF001
|
|
assert lineage is not None
|
|
assert lineage.recovery_generation == 2
|
|
assert service._connection_supervisor.snapshot().host_path.epoch == 1 # noqa: SLF001
|
|
|
|
service._connection_supervisor.observe_host_path( # noqa: SLF001
|
|
HostPathProbeResult(
|
|
available=False,
|
|
fingerprint=None,
|
|
interface=None,
|
|
source_ipv4=None,
|
|
route_class="unavailable",
|
|
reason_code="host-route-unavailable",
|
|
)
|
|
)
|
|
service._connection_supervisor.observe_host_path( # noqa: SLF001
|
|
HostPathProbeResult(
|
|
available=True,
|
|
fingerprint="route-after-wifi-return",
|
|
interface="test0",
|
|
source_ipv4="192.168.1.100",
|
|
route_class="direct",
|
|
)
|
|
)
|
|
assert service._connection_supervisor.snapshot().host_path.epoch == 3 # noqa: SLF001
|
|
assert service._active_stream_recovery_lineage_is_current(lineage) is True # noqa: SLF001
|
|
|
|
physical["reconciliation_ready"] = True
|
|
physical["observed_session_state"] = "scanning"
|
|
control.state = "connection-ready"
|
|
start_projects_before = list(control.start_projects)
|
|
stop_calls_before = control.stop_calls
|
|
historical_ids = {"auto-rebind:2:6", "auto-rebind:2:7"}
|
|
reconcile_ids: list[str] = []
|
|
|
|
def reconcile_resolved_active(*, reconciliation_id: str) -> dict[str, object]:
|
|
if reconciliation_id in historical_ids:
|
|
raise facade_module.PhysicalCommandTransitionError(
|
|
"physical reconciliation id has already been used"
|
|
)
|
|
reconcile_ids.append(reconciliation_id)
|
|
record = physical["record"]
|
|
assert isinstance(record, dict)
|
|
record["revision"] = int(record["revision"]) + 1
|
|
return record
|
|
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"reconcile_resolved_active",
|
|
reconcile_resolved_active,
|
|
)
|
|
monkeypatch.setattr(
|
|
control,
|
|
"request_start",
|
|
lambda **_kwargs: pytest.fail("recovery must not replay START"),
|
|
)
|
|
monkeypatch.setattr(
|
|
control,
|
|
"request_stop",
|
|
lambda **_kwargs: pytest.fail("recovery must not publish STOP"),
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"scan_ble",
|
|
lambda *_args, **_kwargs: pytest.fail("recovery must not touch BLE"),
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"connect",
|
|
lambda *_args, **_kwargs: pytest.fail("recovery must not mutate network state"),
|
|
)
|
|
|
|
decision = asyncio.run(
|
|
service._reconcile_active_stream_physical_state_owned( # noqa: SLF001
|
|
lineage,
|
|
attempt=6,
|
|
)
|
|
)
|
|
|
|
expected_id = facade_module._active_stream_reconciliation_id( # noqa: SLF001
|
|
lineage,
|
|
attempt=6,
|
|
)
|
|
prior_process_lineage = facade_module.dataclass_replace(
|
|
lineage,
|
|
snapshot_runtime_id="snapshot-runtime-prior-process",
|
|
acquisition_id="acq-b0cc1fe4-b934-4a98-a3e5-ecab7afd17c4",
|
|
physical_operation_id="op-prior-acquisition-start",
|
|
)
|
|
prior_process_id = facade_module._active_stream_reconciliation_id( # noqa: SLF001
|
|
prior_process_lineage,
|
|
attempt=6,
|
|
)
|
|
assert decision == "resume"
|
|
assert reconcile_ids == [expected_id]
|
|
assert expected_id not in historical_ids
|
|
assert expected_id != prior_process_id
|
|
assert expected_id == facade_module._active_stream_reconciliation_id( # noqa: SLF001
|
|
lineage,
|
|
attempt=6,
|
|
)
|
|
assert len(expected_id + ".device-info") <= 160
|
|
# Physical rebind only authorizes transport recovery. Visible recovery is
|
|
# still fenced on the later non-empty post-published PCL.
|
|
assert runtime.snapshot()["phase"] == "reconnecting"
|
|
assert runtime.snapshot()["source_ready"] is False
|
|
assert control.start_projects == start_projects_before
|
|
assert control.stop_calls == stop_calls_before
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("observed_state", "expected_reason", "reconcile_expected"),
|
|
[
|
|
("ready", "device-reported-standby", True),
|
|
("scan_over", "device-reported-scan-over", True),
|
|
],
|
|
)
|
|
def test_active_stream_recovery_device_standby_never_restarts_scanner(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
observed_state: str,
|
|
expected_reason: str,
|
|
reconcile_expected: bool,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
)
|
|
service.state()
|
|
lineage = service._active_stream_recovery_lineage # noqa: SLF001
|
|
assert lineage is not None
|
|
physical["reconciliation_ready"] = True
|
|
physical["observed_session_state"] = observed_state
|
|
control.state = "connection-ready"
|
|
start_projects_before = list(control.start_projects)
|
|
stop_calls_before = control.stop_calls
|
|
reconcile_ids: list[str] = []
|
|
|
|
def reconcile_resolved_active(*, reconciliation_id: str) -> dict[str, object]:
|
|
reconcile_ids.append(reconciliation_id)
|
|
record = physical["record"]
|
|
assert isinstance(record, dict)
|
|
record["revision"] = int(record["revision"]) + 1
|
|
physical["resolved_active_recovery_required"] = False
|
|
physical["resolved_scan_over_recovery_required"] = observed_state == "scan_over"
|
|
physical["requires_reconciliation"] = observed_state == "scan_over"
|
|
return record
|
|
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"reconcile_resolved_active",
|
|
reconcile_resolved_active,
|
|
)
|
|
monkeypatch.setattr(
|
|
control,
|
|
"request_start",
|
|
lambda **_kwargs: pytest.fail("standby recovery must not repeat START"),
|
|
)
|
|
monkeypatch.setattr(
|
|
control,
|
|
"request_stop",
|
|
lambda **_kwargs: pytest.fail("standby recovery must not send STOP"),
|
|
)
|
|
|
|
decision = asyncio.run(
|
|
service._reconcile_active_stream_physical_state_owned( # noqa: SLF001
|
|
lineage,
|
|
attempt=4,
|
|
)
|
|
)
|
|
|
|
assert decision == "standby"
|
|
assert bool(reconcile_ids) is reconcile_expected
|
|
assert control.start_projects == start_projects_before
|
|
assert control.stop_calls == stop_calls_before
|
|
assert service._active_stream_recovery_state == "standby" # noqa: SLF001
|
|
assert service._active_stream_recovery_reason_code == expected_reason # noqa: SLF001
|
|
assert physical["resolved_active_recovery_required"] is False
|
|
assert physical["requires_reconciliation"] is (observed_state == "scan_over")
|
|
|
|
|
|
def test_active_stream_recovery_wrong_identity_blocks_without_retry_or_camera_reopen(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, _physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
)
|
|
service.state()
|
|
lineage = service._active_stream_recovery_lineage # noqa: SLF001
|
|
assert lineage is not None
|
|
start_projects_before = list(control.start_projects)
|
|
stop_calls_before = control.stop_calls
|
|
|
|
async def monitor_quiescent() -> bool:
|
|
return True
|
|
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_await_connection_monitor_quiescence",
|
|
monitor_quiescent,
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_acquire_k1_lifecycle_process_lease",
|
|
lambda _holder: None,
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_release_k1_lifecycle_process_lease",
|
|
lambda _holder: None,
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_probe_control_endpoint",
|
|
lambda _target, **_kwargs: facade_module._CorrelatedEndpointObservation( # noqa: SLF001
|
|
path=_direct_host_path(lineage.target_ipv4),
|
|
reachable=True,
|
|
reason_code=None,
|
|
),
|
|
)
|
|
|
|
async def reject_wrong_identity(**_kwargs: object) -> None:
|
|
raise facade_module.ConnectionVerificationError(
|
|
"same IP belongs to another K1",
|
|
reason_code="control-bootstrap-device-identity-unverified",
|
|
)
|
|
|
|
camera_reopens: list[str] = []
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_bootstrap_prestart_control_ready_owned",
|
|
reject_wrong_identity,
|
|
)
|
|
monkeypatch.setattr(
|
|
service.camera_preview,
|
|
"select",
|
|
lambda *_args: camera_reopens.append("camera") or {},
|
|
)
|
|
|
|
decision = asyncio.run(
|
|
service._recover_active_stream_connection_owned(lineage, 1) # noqa: SLF001
|
|
)
|
|
|
|
assert decision == "blocked"
|
|
assert service._active_stream_recovery_state == "blocked" # noqa: SLF001
|
|
assert service._active_stream_recovery_reason_code == ( # noqa: SLF001
|
|
"control-bootstrap-device-identity-unverified"
|
|
)
|
|
assert camera_reopens == []
|
|
assert control.start_projects == start_projects_before
|
|
assert control.stop_calls == stop_calls_before
|
|
|
|
|
|
def test_active_stream_recovery_owned_path_is_read_only_and_resumes_exact_lineage(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, _physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
)
|
|
service.state()
|
|
lineage = service._active_stream_recovery_lineage # noqa: SLF001
|
|
assert lineage is not None
|
|
control.state = "idle"
|
|
start_projects_before = list(control.start_projects)
|
|
stop_calls_before = control.stop_calls
|
|
events: list[tuple[str, object]] = []
|
|
|
|
async def monitor_quiescent() -> bool:
|
|
events.append(("monitor", True))
|
|
return True
|
|
|
|
def probe_exact_target( # noqa: SLF001
|
|
target_ipv4: str,
|
|
*,
|
|
association_timeout_seconds: float,
|
|
) -> facade_module._CorrelatedEndpointObservation:
|
|
events.append(
|
|
("probe", (target_ipv4, association_timeout_seconds))
|
|
)
|
|
return facade_module._CorrelatedEndpointObservation( # noqa: SLF001
|
|
path=_direct_host_path(target_ipv4),
|
|
reachable=True,
|
|
reason_code=None,
|
|
)
|
|
|
|
async def inspection_only_bootstrap(**kwargs: object) -> None:
|
|
assert kwargs["connection_mode"] == lineage.connection_mode
|
|
assert kwargs["inspection_only"] is True
|
|
events.append(("device-info-status", kwargs["parent_operation_id"]))
|
|
|
|
async def reconcile_same_lineage(
|
|
candidate: facade_module._ActiveStreamRecoveryLineage, # noqa: SLF001
|
|
*,
|
|
attempt: int,
|
|
) -> facade_module.RecoveryDecision:
|
|
assert candidate is lineage
|
|
events.append(("physical-read-only", attempt))
|
|
return "resume"
|
|
|
|
def recover_local_camera(
|
|
candidate: facade_module._ActiveStreamRecoveryLineage, # noqa: SLF001
|
|
) -> facade_module.RecoveryDecision:
|
|
assert candidate is lineage
|
|
events.append(("camera-local", candidate.evidence_session_id))
|
|
return "resume"
|
|
|
|
def forbidden_device_command(**_kwargs: object) -> object:
|
|
pytest.fail("active stream recovery must not publish START or STOP")
|
|
|
|
async def forbidden_ble_or_network_mutation(*_args: object, **_kwargs: object) -> object:
|
|
pytest.fail("active stream recovery must not scan, write BLE, or provision Wi-Fi")
|
|
|
|
monkeypatch.setattr(service, "_await_connection_monitor_quiescence", monitor_quiescent)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_acquire_k1_lifecycle_process_lease",
|
|
lambda holder: events.append(("lease-acquire", holder)),
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_release_k1_lifecycle_process_lease",
|
|
lambda holder: events.append(("lease-release", holder)),
|
|
)
|
|
monkeypatch.setattr(service, "_probe_control_endpoint", probe_exact_target)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_bootstrap_prestart_control_ready_owned",
|
|
inspection_only_bootstrap,
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_reconcile_active_stream_physical_state_owned",
|
|
reconcile_same_lineage,
|
|
)
|
|
monkeypatch.setattr(service, "_recover_active_stream_camera_owned", recover_local_camera)
|
|
monkeypatch.setattr(control, "request_start", forbidden_device_command)
|
|
monkeypatch.setattr(control, "request_stop", forbidden_device_command)
|
|
monkeypatch.setattr(service, "scan_ble", forbidden_ble_or_network_mutation)
|
|
monkeypatch.setattr(service, "connect", forbidden_ble_or_network_mutation)
|
|
|
|
decision = asyncio.run(
|
|
service._recover_active_stream_connection_owned(lineage, 5) # noqa: SLF001
|
|
)
|
|
|
|
assert decision == "resume"
|
|
assert events == [
|
|
("monitor", True),
|
|
("lease-acquire", "network"),
|
|
(
|
|
"probe",
|
|
(
|
|
lineage.target_ipv4,
|
|
facade_module.ACTIVE_STREAM_RECOVERY_ASSOCIATION_TIMEOUT_SECONDS,
|
|
),
|
|
),
|
|
(
|
|
"device-info-status",
|
|
(
|
|
f"active-stream-recovery:{lineage.acquisition_id}:"
|
|
f"{lineage.recovery_generation}"
|
|
),
|
|
),
|
|
("physical-read-only", 5),
|
|
("lease-release", "network"),
|
|
]
|
|
# Camera recovery is now intentionally downstream of the post-Rerun
|
|
# checkpoint rebind. Control recovery alone cannot open/promote media.
|
|
assert ("camera-local", lineage.evidence_session_id) not in events
|
|
assert control.start_projects == start_projects_before
|
|
assert control.stop_calls == stop_calls_before
|
|
assert service._active_stream_recovery_lineage_is_current(lineage) is True # noqa: SLF001
|
|
assert service._active_stream_recovery_reason_code == "mqtt-resubscribe-pending" # noqa: SLF001
|
|
|
|
|
|
def test_incident_recovery_retires_epoch_one_control_and_retries_fresh_inspection_binding(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Regress the 2026-08-12 acq-209faf83 route-loss failure.
|
|
|
|
The incident START/control owner was bound to host-path epoch 1. Wi-Fi
|
|
loss rotated that epoch, and the first fresh inspection raced one more
|
|
association edge. That expected local binding loss must keep retrying;
|
|
the next inspection must bind the current epoch without replaying any
|
|
physical or network command.
|
|
"""
|
|
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
|
|
class IncidentControl(FakeInteractiveControlSession):
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.opened_bindings: list[ApplicationConnectionBinding] = []
|
|
self.close_calls = 0
|
|
self.retire_calls = 0
|
|
self.binding_validation_calls = 0
|
|
|
|
def validate_connection_binding(self) -> None:
|
|
self.binding_validation_calls += 1
|
|
raise AssertionError("recovery must not validate/reuse the epoch-1 owner")
|
|
|
|
def close(self) -> None:
|
|
self.close_calls += 1
|
|
super().close()
|
|
|
|
def retire_for_network_change(self, **kwargs: object) -> dict[str, object]:
|
|
self.retire_calls += 1
|
|
return super().retire_for_network_change(**kwargs)
|
|
|
|
def open(
|
|
self,
|
|
*,
|
|
connection_binding: ApplicationConnectionBinding,
|
|
inspection_only: bool = False,
|
|
**kwargs: object,
|
|
) -> dict[str, object]:
|
|
self.opened_bindings.append(connection_binding)
|
|
if len(self.opened_bindings) == 1:
|
|
# Match the live failure: the newly opened worker observed one
|
|
# more route/association epoch before its first MQTT publish.
|
|
service._connection_supervisor.observe_host_path( # noqa: SLF001
|
|
recovered_path_b
|
|
)
|
|
self.state = "failed"
|
|
self.failure = {
|
|
"reason_code": "application-connection-binding-lost",
|
|
"safe_to_retry": True,
|
|
"modeling_command_attempted": False,
|
|
}
|
|
return self.snapshot()
|
|
self.session_generation += 1
|
|
super().open(
|
|
connection_binding=connection_binding,
|
|
inspection_only=inspection_only,
|
|
**kwargs,
|
|
)
|
|
assert self.verified_control is not None
|
|
self.verified_control = {
|
|
**self.verified_control,
|
|
"control_session_id": (
|
|
f"fake-recovery-control-{self.session_generation}"
|
|
),
|
|
"producer_generation": self.session_generation,
|
|
}
|
|
return self.snapshot()
|
|
|
|
control = IncidentControl()
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
_control, _physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
runtime_phase="reconnecting",
|
|
camera_phase="streaming",
|
|
)
|
|
# The fixture installs its own fake session; replace it before freezing the
|
|
# recovery lineage while retaining the exact already-resolved START proof.
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
control.state = "scanning"
|
|
original_epoch = service._connection_supervisor.snapshot().host_path.epoch # noqa: SLF001
|
|
assert original_epoch == 1
|
|
service.state()
|
|
lineage = service._active_stream_recovery_lineage # noqa: SLF001
|
|
assert lineage is not None
|
|
|
|
lost_path = HostPathProbeResult(
|
|
available=False,
|
|
fingerprint=None,
|
|
interface=None,
|
|
source_ipv4=None,
|
|
route_class="unavailable",
|
|
reason_code="host-route-unavailable",
|
|
)
|
|
recovered_path_a = HostPathProbeResult(
|
|
available=True,
|
|
fingerprint="incident-route-after-wifi-return-a",
|
|
interface="test0",
|
|
source_ipv4="192.168.68.100",
|
|
route_class="direct",
|
|
)
|
|
recovered_path_b = HostPathProbeResult(
|
|
available=True,
|
|
fingerprint="incident-route-after-wifi-return-b",
|
|
interface="test0",
|
|
source_ipv4="192.168.68.100",
|
|
route_class="direct",
|
|
)
|
|
service._connection_supervisor.observe_host_path(lost_path) # noqa: SLF001
|
|
assert service._connection_supervisor.snapshot().host_path.epoch == 2 # noqa: SLF001
|
|
|
|
probe_paths = iter((recovered_path_a, recovered_path_b))
|
|
|
|
def probe_current_epoch(
|
|
target_ipv4: str,
|
|
**_kwargs: object,
|
|
) -> facade_module._CorrelatedEndpointObservation: # noqa: SLF001
|
|
path = next(probe_paths)
|
|
service._observe_connection_transport( # noqa: SLF001
|
|
target_ipv4,
|
|
path=path,
|
|
reachable=True,
|
|
)
|
|
return facade_module._CorrelatedEndpointObservation( # noqa: SLF001
|
|
path=path,
|
|
reachable=True,
|
|
reason_code=None,
|
|
)
|
|
|
|
async def monitor_quiescent() -> bool:
|
|
return True
|
|
|
|
async def reconcile_same_lineage(
|
|
candidate: facade_module._ActiveStreamRecoveryLineage, # noqa: SLF001
|
|
*,
|
|
attempt: int,
|
|
) -> facade_module.RecoveryDecision:
|
|
assert candidate is lineage
|
|
assert attempt == 6
|
|
return "resume"
|
|
|
|
service._bootstrap_prestart_control_ready_owned = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
XgridsK1CompatibilityService._bootstrap_prestart_control_ready_owned, # noqa: SLF001
|
|
service,
|
|
)
|
|
monkeypatch.setattr(service, "_await_connection_monitor_quiescence", monitor_quiescent)
|
|
monkeypatch.setattr(service, "_probe_control_endpoint", probe_current_epoch)
|
|
monkeypatch.setattr(service, "_acquire_k1_lifecycle_process_lease", lambda _holder: None)
|
|
monkeypatch.setattr(service, "_release_k1_lifecycle_process_lease", lambda _holder: None)
|
|
monkeypatch.setattr(service, "_acquire_application_control_process_lease", lambda: None)
|
|
monkeypatch.setattr(service, "_reconcile_application_control_process_lease", lambda _s: None)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_reconcile_active_stream_physical_state_owned",
|
|
reconcile_same_lineage,
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_recover_active_stream_camera_owned",
|
|
lambda candidate: "resume" if candidate is lineage else "blocked",
|
|
)
|
|
monkeypatch.setattr(
|
|
control,
|
|
"request_start",
|
|
lambda **_kwargs: pytest.fail("recovery must not replay START"),
|
|
)
|
|
monkeypatch.setattr(
|
|
control,
|
|
"request_stop",
|
|
lambda **_kwargs: pytest.fail("recovery must not publish STOP"),
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"scan_ble",
|
|
lambda *_args, **_kwargs: pytest.fail("recovery must not touch BLE"),
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"connect",
|
|
lambda *_args, **_kwargs: pytest.fail("recovery must not mutate network state"),
|
|
)
|
|
|
|
first = asyncio.run(
|
|
service._recover_active_stream_connection_owned(lineage, 5) # noqa: SLF001
|
|
)
|
|
|
|
assert first == "retry"
|
|
assert service._active_stream_recovery_state == "reconnecting" # noqa: SLF001
|
|
assert service._active_stream_recovery_reason_code == ( # noqa: SLF001
|
|
"application-connection-binding-lost"
|
|
)
|
|
assert control.opened_bindings[0].host_path_epoch == 3
|
|
assert control.close_calls == 1
|
|
assert control.retire_calls == 2
|
|
assert control.binding_validation_calls == 0
|
|
|
|
second = asyncio.run(
|
|
service._recover_active_stream_connection_owned(lineage, 6) # noqa: SLF001
|
|
)
|
|
|
|
assert second == "resume"
|
|
assert [binding.host_path_epoch for binding in control.opened_bindings] == [3, 4]
|
|
assert control.opened_bindings[-1].host_path_epoch == (
|
|
service._connection_supervisor.snapshot().host_path.epoch # noqa: SLF001
|
|
)
|
|
assert control.inspection_only is True
|
|
assert control.state == "connection-ready"
|
|
assert service._active_stream_recovery_lineage_is_current(lineage) is True # noqa: SLF001
|
|
assert service._active_stream_recovery_reason_code == "mqtt-resubscribe-pending" # noqa: SLF001
|
|
|
|
|
|
def test_active_stream_recovery_waits_for_terminal_control_worker_retirement(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, _physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
)
|
|
service.state()
|
|
lineage = service._active_stream_recovery_lineage # noqa: SLF001
|
|
assert lineage is not None
|
|
control.state = "failed"
|
|
control.failure = {
|
|
"reason_code": "mqtt_network_loop_failed",
|
|
"modeling_command_attempted": True,
|
|
"safe_to_retry": False,
|
|
}
|
|
retire_attempts = 0
|
|
|
|
def retire_terminal_control(**_kwargs: object) -> dict[str, object]:
|
|
nonlocal retire_attempts
|
|
retire_attempts += 1
|
|
if retire_attempts == 1:
|
|
raise ApplicationAcceptanceError("control session worker is still retiring")
|
|
control.state = "idle"
|
|
control.verified_control = None
|
|
return control.snapshot()
|
|
|
|
async def monitor_quiescent() -> bool:
|
|
return True
|
|
|
|
async def inspection_only_bootstrap(**_kwargs: object) -> None:
|
|
return None
|
|
|
|
async def reconcile_same_lineage(
|
|
candidate: facade_module._ActiveStreamRecoveryLineage, # noqa: SLF001
|
|
*,
|
|
attempt: int,
|
|
) -> facade_module.RecoveryDecision:
|
|
assert candidate is lineage
|
|
assert attempt == 2
|
|
return "resume"
|
|
|
|
monkeypatch.setattr(control, "retire_for_network_change", retire_terminal_control)
|
|
monkeypatch.setattr(service, "_await_connection_monitor_quiescence", monitor_quiescent)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_acquire_k1_lifecycle_process_lease",
|
|
lambda _holder: None,
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_release_k1_lifecycle_process_lease",
|
|
lambda _holder: None,
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_probe_control_endpoint",
|
|
lambda _target, **_kwargs: facade_module._CorrelatedEndpointObservation( # noqa: SLF001
|
|
path=_direct_host_path(lineage.target_ipv4),
|
|
reachable=True,
|
|
reason_code=None,
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_bootstrap_prestart_control_ready_owned",
|
|
inspection_only_bootstrap,
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_reconcile_active_stream_physical_state_owned",
|
|
reconcile_same_lineage,
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_recover_active_stream_camera_owned",
|
|
lambda candidate: "resume" if candidate is lineage else "blocked",
|
|
)
|
|
|
|
decision = asyncio.run(
|
|
service._recover_active_stream_connection_owned(lineage, 2) # noqa: SLF001
|
|
)
|
|
|
|
assert decision == "resume"
|
|
assert retire_attempts == 2
|
|
assert service._active_stream_recovery_state == "reconnecting" # noqa: SLF001
|
|
assert service._active_stream_recovery_reason_code == "mqtt-resubscribe-pending" # noqa: SLF001
|
|
|
|
|
|
def test_active_stream_recovery_exact_device_system_fault_is_terminal_without_commands(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, _physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
)
|
|
service.state()
|
|
lineage = service._active_stream_recovery_lineage # noqa: SLF001
|
|
assert lineage is not None
|
|
control.state = "idle"
|
|
start_projects_before = list(control.start_projects)
|
|
stop_calls_before = control.stop_calls
|
|
|
|
async def monitor_quiescent() -> bool:
|
|
return True
|
|
|
|
async def bootstrap_system_fault(**kwargs: object) -> None:
|
|
assert kwargs["inspection_only"] is True
|
|
control.state = "failed"
|
|
control.failure = {
|
|
"reason_code": "application-device-system-error",
|
|
"modeling_command_attempted": False,
|
|
"safe_to_retry": False,
|
|
}
|
|
|
|
monkeypatch.setattr(service, "_await_connection_monitor_quiescence", monitor_quiescent)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_acquire_k1_lifecycle_process_lease",
|
|
lambda _holder: None,
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_release_k1_lifecycle_process_lease",
|
|
lambda _holder: None,
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_probe_control_endpoint",
|
|
lambda _target, **_kwargs: facade_module._CorrelatedEndpointObservation( # noqa: SLF001
|
|
path=_direct_host_path(lineage.target_ipv4),
|
|
reachable=True,
|
|
reason_code=None,
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_bootstrap_prestart_control_ready_owned",
|
|
bootstrap_system_fault,
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_recover_active_stream_camera_owned",
|
|
lambda _candidate: pytest.fail("system fault must not reopen camera"),
|
|
)
|
|
monkeypatch.setattr(
|
|
control,
|
|
"request_start",
|
|
lambda **_kwargs: pytest.fail("system fault recovery must not repeat START"),
|
|
)
|
|
monkeypatch.setattr(
|
|
control,
|
|
"request_stop",
|
|
lambda **_kwargs: pytest.fail("system fault recovery must not send STOP"),
|
|
)
|
|
|
|
decision = asyncio.run(
|
|
service._recover_active_stream_connection_owned(lineage, 6) # noqa: SLF001
|
|
)
|
|
|
|
assert decision == "fault"
|
|
assert service._active_stream_recovery_state == "fault" # noqa: SLF001
|
|
assert service._active_stream_recovery_reason_code == ( # noqa: SLF001
|
|
"active-stream-recovery-system-error"
|
|
)
|
|
assert service._active_stream_recovery_terminal_outcome == "fault" # noqa: SLF001
|
|
assert control.start_projects == start_projects_before
|
|
assert control.stop_calls == stop_calls_before
|
|
|
|
|
|
def test_active_stream_recovery_bootstrap_system_error_is_normalized_terminal_fault(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, _physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
)
|
|
service.state()
|
|
lineage = service._active_stream_recovery_lineage # noqa: SLF001
|
|
assert lineage is not None
|
|
control.state = "idle"
|
|
start_projects_before = list(control.start_projects)
|
|
stop_calls_before = control.stop_calls
|
|
monkeypatch.setattr(service, "_seal_acquisition_capture_clock", lambda: None)
|
|
|
|
async def monitor_quiescent() -> bool:
|
|
return True
|
|
|
|
async def bootstrap_system_error(**kwargs: object) -> None:
|
|
assert kwargs["inspection_only"] is True
|
|
raise facade_module.ConnectionVerificationError(
|
|
"DeviceInfo reported a system fault",
|
|
reason_code="application-device-system-error",
|
|
)
|
|
|
|
monkeypatch.setattr(service, "_await_connection_monitor_quiescence", monitor_quiescent)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_acquire_k1_lifecycle_process_lease",
|
|
lambda _holder: None,
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_release_k1_lifecycle_process_lease",
|
|
lambda _holder: None,
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_probe_control_endpoint",
|
|
lambda _target, **_kwargs: facade_module._CorrelatedEndpointObservation( # noqa: SLF001
|
|
path=_direct_host_path(lineage.target_ipv4),
|
|
reachable=True,
|
|
reason_code=None,
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_bootstrap_prestart_control_ready_owned",
|
|
bootstrap_system_error,
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_reconcile_active_stream_physical_state_owned",
|
|
lambda *_args, **_kwargs: pytest.fail(
|
|
"bootstrap system fault must stop before physical reconciliation"
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_recover_active_stream_camera_owned",
|
|
lambda _candidate: pytest.fail("bootstrap system fault must not reopen camera"),
|
|
)
|
|
monkeypatch.setattr(
|
|
control,
|
|
"request_start",
|
|
lambda **_kwargs: pytest.fail("system fault recovery must not repeat START"),
|
|
)
|
|
monkeypatch.setattr(
|
|
control,
|
|
"request_stop",
|
|
lambda **_kwargs: pytest.fail("system fault recovery must not send STOP"),
|
|
)
|
|
|
|
decision = asyncio.run(
|
|
service._recover_active_stream_connection_owned(lineage, 7) # noqa: SLF001
|
|
)
|
|
|
|
assert decision == "fault"
|
|
assert service._active_stream_recovery_state == "fault" # noqa: SLF001
|
|
assert service._active_stream_recovery_reason_code == ( # noqa: SLF001
|
|
"active-stream-recovery-system-error"
|
|
)
|
|
assert service._active_stream_recovery_terminal_outcome == "fault" # noqa: SLF001
|
|
assert control.start_projects == start_projects_before
|
|
assert control.stop_calls == stop_calls_before
|
|
|
|
terminal = service.state()
|
|
|
|
assert terminal["acquisition"]["state"] == "failed"
|
|
assert terminal["acquisition"]["result"]["device_state"] == "fault"
|
|
assert terminal["source_mode"] == "idle"
|
|
assert runtime.stop_calls == 1
|
|
assert control.start_projects == start_projects_before
|
|
assert control.stop_calls == stop_calls_before
|
|
|
|
|
|
def test_active_stream_recovery_never_restarts_nontransport_camera_error(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
_control, _physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
)
|
|
service.state()
|
|
lineage = service._active_stream_recovery_lineage # noqa: SLF001
|
|
assert lineage is not None
|
|
camera = service.camera_preview.snapshot()
|
|
camera["phase"] = "error"
|
|
camera["error"] = {
|
|
"code": "camera-artifact-write-failed",
|
|
"message": "synthetic local artifact failure",
|
|
}
|
|
restart_calls: list[str] = []
|
|
monkeypatch.setattr(
|
|
service.camera_preview,
|
|
"restart_recording_producer",
|
|
lambda *_args, **_kwargs: restart_calls.append("restart") or {},
|
|
)
|
|
|
|
decision = service._recover_active_stream_camera_owned(lineage) # noqa: SLF001
|
|
|
|
assert decision == "blocked"
|
|
assert restart_calls == []
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("camera_phase", "camera_error", "producer_alive", "active_epoch"),
|
|
[
|
|
("connecting", None, False, 7),
|
|
(
|
|
"error",
|
|
{
|
|
"code": "invalid-fmp4",
|
|
"message": "synthetic pre-first-media camera failure",
|
|
},
|
|
False,
|
|
7,
|
|
),
|
|
(
|
|
"error",
|
|
{
|
|
"code": "invalid-fmp4",
|
|
"message": "synthetic detached pre-finalize camera failure",
|
|
},
|
|
False,
|
|
None,
|
|
),
|
|
],
|
|
)
|
|
def test_active_stream_recovery_keeps_pre_first_media_camera_downstream_of_mqtt(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
camera_phase: str,
|
|
camera_error: dict[str, str] | None,
|
|
producer_alive: bool,
|
|
active_epoch: int | None,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, _physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
camera_phase="streaming",
|
|
)
|
|
service.state()
|
|
lineage = service._active_stream_recovery_lineage # noqa: SLF001
|
|
assert lineage is not None
|
|
start_projects_before = list(control.start_projects)
|
|
stop_calls_before = control.stop_calls
|
|
camera = service.camera_preview.snapshot()
|
|
camera.update(
|
|
{
|
|
"phase": camera_phase,
|
|
"generation": 7,
|
|
"active_source_id": facade_module.DEFAULT_ACQUISITION_CAMERA_SOURCE,
|
|
"recording": {
|
|
"active": True,
|
|
"session": lineage.evidence_session_id,
|
|
"active_epoch": active_epoch,
|
|
"producer_alive": producer_alive,
|
|
"producer_age_ms": (
|
|
facade_module.ACTIVE_STREAM_CAMERA_STALL_MILLISECONDS + 1
|
|
),
|
|
"last_segment_age_ms": None,
|
|
"committed_media_segment_count": 0,
|
|
"completed_epochs": 0,
|
|
"last_summary": None,
|
|
},
|
|
"error": camera_error,
|
|
}
|
|
)
|
|
activation_lineage = (
|
|
lineage.acquisition_id,
|
|
lineage.evidence_session_id,
|
|
lineage.runtime_producer_generation,
|
|
)
|
|
with service._lock: # noqa: SLF001
|
|
service._camera_activation_lineage = activation_lineage # noqa: SLF001
|
|
restart_calls: list[str] = []
|
|
monkeypatch.setattr(
|
|
service.camera_preview,
|
|
"restart_recording_producer",
|
|
lambda *_args, **_kwargs: restart_calls.append("restart") or {},
|
|
)
|
|
|
|
decision = service._recover_active_stream_camera_owned(lineage) # noqa: SLF001
|
|
|
|
assert decision == "resume"
|
|
assert restart_calls == []
|
|
with service._lock: # noqa: SLF001
|
|
if camera_phase == "error" or producer_alive is False:
|
|
assert service._camera_activation_lineage is None # noqa: SLF001
|
|
assert service._camera_activation_retry_lineage == activation_lineage # noqa: SLF001
|
|
else:
|
|
assert service._camera_activation_lineage == activation_lineage # noqa: SLF001
|
|
state = service.state()
|
|
assert state["phase"] == "reconnecting"
|
|
assert state["acquisition"]["state"] == "acquiring"
|
|
assert state["camera_preview"]["activation_admission"]["state"] == "activating"
|
|
assert runtime.stop_calls == 0
|
|
assert control.start_projects == start_projects_before
|
|
assert control.stop_calls == stop_calls_before
|
|
|
|
|
|
def test_active_stream_recovery_never_retries_invalid_fmp4_after_media_commit(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, _physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
camera_phase="streaming",
|
|
)
|
|
service.state()
|
|
lineage = service._active_stream_recovery_lineage # noqa: SLF001
|
|
assert lineage is not None
|
|
camera = service.camera_preview.snapshot()
|
|
camera.update(
|
|
{
|
|
"phase": "error",
|
|
"generation": 7,
|
|
"active_source_id": facade_module.DEFAULT_ACQUISITION_CAMERA_SOURCE,
|
|
"recording": {
|
|
"active": True,
|
|
"session": lineage.evidence_session_id,
|
|
"active_epoch": 7,
|
|
"committed_media_segment_count": 1,
|
|
"producer_alive": False,
|
|
"producer_age_ms": 1_000,
|
|
"last_segment_age_ms": 0,
|
|
"completed_epochs": 0,
|
|
"last_summary": None,
|
|
},
|
|
"error": {
|
|
"code": "invalid-fmp4",
|
|
"message": "synthetic corruption after the first media commit",
|
|
},
|
|
}
|
|
)
|
|
activation_lineage = (
|
|
lineage.acquisition_id,
|
|
lineage.evidence_session_id,
|
|
lineage.runtime_producer_generation,
|
|
)
|
|
with service._lock: # noqa: SLF001
|
|
service._camera_activation_lineage = activation_lineage # noqa: SLF001
|
|
restart_calls: list[str] = []
|
|
monkeypatch.setattr(
|
|
service.camera_preview,
|
|
"restart_recording_producer",
|
|
lambda *_args, **_kwargs: restart_calls.append("restart") or {},
|
|
)
|
|
monkeypatch.setattr(service, "_seal_acquisition_capture_clock", lambda: None)
|
|
|
|
assert service._recover_active_stream_camera_owned(lineage) == "blocked" # noqa: SLF001
|
|
assert restart_calls == []
|
|
with service._lock: # noqa: SLF001
|
|
assert service._camera_activation_lineage == activation_lineage # noqa: SLF001
|
|
assert service._camera_activation_retry_lineage is None # noqa: SLF001
|
|
|
|
terminal = service.state()
|
|
assert terminal["acquisition"]["state"] == "failed"
|
|
assert terminal["acquisition"]["result"]["camera_failure_code"] == "invalid-fmp4"
|
|
assert runtime.stop_calls == 1
|
|
assert control.stop_calls == 0
|
|
|
|
|
|
def test_bound_scan_over_without_stop_dominates_late_empty_camera_epoch_failure(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
runtime_phase="live",
|
|
camera_phase="streaming",
|
|
)
|
|
camera = service.camera_preview.snapshot()
|
|
recording = camera["recording"]
|
|
assert isinstance(recording, dict)
|
|
recording.update(
|
|
{
|
|
"active": True,
|
|
"active_epoch": 2,
|
|
"committed_media_segment_count": 4_833,
|
|
"producer_alive": False,
|
|
"completed_epochs": 2,
|
|
"last_summary": {
|
|
"codec_epoch": 2,
|
|
"status": "failed",
|
|
"media_segment_count": 0,
|
|
"failure_code": "invalid-fmp4",
|
|
},
|
|
}
|
|
)
|
|
camera.update(
|
|
{
|
|
"phase": "error",
|
|
"error": {
|
|
"code": "invalid-fmp4",
|
|
"message": "synthetic empty post-power-loss epoch",
|
|
},
|
|
}
|
|
)
|
|
physical.update(
|
|
{
|
|
"runtime_bound": True,
|
|
"reconciliation_ready": True,
|
|
"observed_session_state": "scan_over",
|
|
}
|
|
)
|
|
control.state = "failed"
|
|
control.state_revision += 1
|
|
control.failure = {
|
|
"reason_code": "application_acceptance_failed",
|
|
"failed_phase": "scanning",
|
|
"modeling_command_attempted": True,
|
|
"stop_command_attempted": False,
|
|
"diagnostic_snapshot_unavailable": [],
|
|
"diagnostic_evidence_unavailable": [],
|
|
"safe_to_retry": False,
|
|
}
|
|
|
|
def scan_over_control_snapshot() -> dict[str, object]:
|
|
snapshot = FakeInteractiveControlSession.snapshot(control)
|
|
snapshot["transport"] = {
|
|
"state": "failed",
|
|
"publish_attempts": 7,
|
|
"device_status_reports": 9,
|
|
"latest_device_session_state": "scan_over",
|
|
"latest_device_project_bound": True,
|
|
"latest_device_init_ready": False,
|
|
"latest_system_error_code": None,
|
|
"automatic_retry": False,
|
|
"automatic_reconnect": False,
|
|
}
|
|
return snapshot
|
|
|
|
monkeypatch.setattr(control, "snapshot", scan_over_control_snapshot)
|
|
monkeypatch.setattr(service, "_seal_acquisition_capture_clock", lambda: None)
|
|
camera_stop_calls: list[str] = []
|
|
monkeypatch.setattr(
|
|
service.camera_preview,
|
|
"stop_current",
|
|
lambda: camera_stop_calls.append("stop") or {},
|
|
)
|
|
start_projects_before = list(control.start_projects)
|
|
stop_calls_before = control.stop_calls
|
|
|
|
terminal = service.state()
|
|
|
|
assert terminal["acquisition"]["state"] == "interrupted"
|
|
assert terminal["acquisition"]["message_code"] == (
|
|
"acquisition.recovery.device_standby_observed"
|
|
)
|
|
assert terminal["acquisition"]["result"] == {
|
|
"receiver_stopped": True,
|
|
"device_state": "scan_over",
|
|
"device_stop": "not-sent",
|
|
"automatic_command_retry": False,
|
|
"read_only_recovery": True,
|
|
"physical_reconciliation_required": True,
|
|
}
|
|
assert terminal["acquisition"]["cleanup_pending"] is False
|
|
assert camera_stop_calls == ["stop"]
|
|
assert runtime.stop_calls == 1
|
|
assert control.start_projects == start_projects_before
|
|
assert control.stop_calls == stop_calls_before == 0
|
|
|
|
|
|
def test_active_stream_recovery_reopens_exact_camera_epoch_once_and_fences_stale_lineage(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
_control, _physical = _install_composite_active_recovery_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
)
|
|
state = service.state()
|
|
lineage = service._active_stream_recovery_lineage # noqa: SLF001
|
|
assert lineage is not None
|
|
camera = dict(state["camera_preview"])
|
|
camera["phase"] = "streaming"
|
|
camera["generation"] = 7
|
|
camera["active_source_id"] = facade_module.DEFAULT_ACQUISITION_CAMERA_SOURCE
|
|
camera["recording"] = {
|
|
"active": True,
|
|
"session": lineage.evidence_session_id,
|
|
"active_epoch": 7,
|
|
"producer_alive": True,
|
|
"producer_age_ms": facade_module.ACTIVE_STREAM_CAMERA_STALL_MILLISECONDS + 1,
|
|
"last_segment_age_ms": (facade_module.ACTIVE_STREAM_CAMERA_STALL_MILLISECONDS + 1),
|
|
"committed_media_segment_count": 4,
|
|
}
|
|
camera_calls: list[tuple[str, str, int, str, int | None, int]] = []
|
|
perception_binds: list[tuple[str, int]] = []
|
|
|
|
def snapshot_camera() -> dict[str, object]:
|
|
return dict(camera)
|
|
|
|
def reopen_camera(
|
|
source_id: str,
|
|
target: str,
|
|
*,
|
|
expected_generation: int,
|
|
expected_recording_session: str,
|
|
expected_active_epoch: int | None,
|
|
expected_recording_media_segment_count: int,
|
|
pre_detach_fence: Callable[[Callable[[], bool]], bool],
|
|
commit_fence: Callable[[Callable[[], bool]], bool] | None = None,
|
|
committed_before_start: Callable[[dict[str, object]], None] | None = None,
|
|
) -> dict[str, object]:
|
|
camera_calls.append(
|
|
(
|
|
source_id,
|
|
target,
|
|
expected_generation,
|
|
expected_recording_session,
|
|
expected_active_epoch,
|
|
expected_recording_media_segment_count,
|
|
)
|
|
)
|
|
assert pre_detach_fence(lambda: True) is True
|
|
camera.update(
|
|
{
|
|
"phase": "connecting",
|
|
"generation": expected_generation + 1,
|
|
"active_source_id": source_id,
|
|
"recording": {
|
|
"active": True,
|
|
"session": lineage.evidence_session_id,
|
|
"active_epoch": 8,
|
|
"producer_alive": True,
|
|
"producer_age_ms": 0,
|
|
"last_segment_age_ms": 0,
|
|
"last_media_segment_age_ms": None,
|
|
"committed_media_segment_count": 4,
|
|
"media_ready": False,
|
|
"current_epoch": {
|
|
"generation": 8,
|
|
"init_committed": False,
|
|
"init_committed_age_ms": None,
|
|
"first_media_committed": False,
|
|
"first_media_committed_age_ms": None,
|
|
"committed_media_segment_count": 0,
|
|
"last_media_segment_age_ms": None,
|
|
},
|
|
},
|
|
}
|
|
)
|
|
assert commit_fence is not None
|
|
assert committed_before_start is not None
|
|
assert commit_fence(
|
|
lambda: committed_before_start(dict(camera)) is None
|
|
) is True
|
|
return dict(camera)
|
|
|
|
def bind_perception(session: str, reopened: dict[str, object]) -> None:
|
|
recording = reopened["recording"]
|
|
assert isinstance(recording, dict)
|
|
active_epoch = recording["active_epoch"]
|
|
assert isinstance(active_epoch, int)
|
|
perception_binds.append((session, active_epoch))
|
|
|
|
monkeypatch.setattr(service.camera_preview, "snapshot", snapshot_camera)
|
|
monkeypatch.setattr(
|
|
service.camera_preview,
|
|
"restart_recording_producer",
|
|
reopen_camera,
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_bind_live_perception_camera",
|
|
bind_perception,
|
|
)
|
|
runtime.phase = "live"
|
|
runtime.source_ready = True
|
|
runtime.recovery_state = "recovered"
|
|
with service._lock: # noqa: SLF001
|
|
service._active_stream_recovery_state = "recovered" # noqa: SLF001
|
|
|
|
assert ( # noqa: SLF001
|
|
service._recover_active_stream_camera_owned(
|
|
lineage,
|
|
require_runtime_reconnecting=False,
|
|
)
|
|
== "resume"
|
|
)
|
|
assert ( # noqa: SLF001
|
|
service._recover_active_stream_camera_owned(
|
|
lineage,
|
|
require_runtime_reconnecting=False,
|
|
)
|
|
== "resume"
|
|
)
|
|
assert camera_calls == [
|
|
(
|
|
facade_module.DEFAULT_ACQUISITION_CAMERA_SOURCE,
|
|
lineage.target_ipv4,
|
|
7,
|
|
lineage.evidence_session_id,
|
|
7,
|
|
4,
|
|
)
|
|
]
|
|
assert perception_binds == [(lineage.evidence_session_id, 8)]
|
|
assert camera["recording"]["media_ready"] is False
|
|
recovery = service._active_stream_recovery_projection( # noqa: SLF001
|
|
runtime.snapshot(),
|
|
camera=camera,
|
|
)
|
|
assert recovery["camera_recovery"] == "owned"
|
|
assert recovery["camera_media_state"] == "pending-init"
|
|
assert recovery["camera_media_ready"] is False
|
|
assert recovery["camera_epoch"] == {
|
|
"generation": 8,
|
|
"init_committed": False,
|
|
"init_committed_age_ms": None,
|
|
"first_media_committed": False,
|
|
"first_media_committed_age_ms": None,
|
|
"committed_media_segment_count": 0,
|
|
"last_media_segment_age_ms": None,
|
|
}
|
|
|
|
runtime.producer_generation += 1
|
|
assert ( # noqa: SLF001
|
|
service._recover_active_stream_camera_owned(
|
|
lineage,
|
|
require_runtime_reconnecting=False,
|
|
)
|
|
== "blocked"
|
|
)
|
|
assert len(camera_calls) == 1
|
|
|
|
|
|
def test_device_standby_retires_sources_after_terminal_local_stop_failure(
|
|
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
|
|
binding = _seed_supervised_connection(service)
|
|
service._compatibility_attestation = ATTESTATION.model_dump(mode="json") # noqa: SLF001
|
|
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="TEST001",
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(
|
|
_start_request(
|
|
acquisition_id=acquisition_id,
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
)
|
|
start_operation_id = service._acquisition_start_operation_id # noqa: SLF001
|
|
assert isinstance(start_operation_id, str)
|
|
physical_proof = _exact_start_physical_proof(
|
|
operation_id=start_operation_id,
|
|
acquisition_id=acquisition_id,
|
|
binding=binding,
|
|
resolved=True,
|
|
)
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"snapshot",
|
|
lambda: physical_proof,
|
|
)
|
|
runtime.mark_ready()
|
|
runtime.pcl_frames = 1
|
|
service.state()
|
|
|
|
stopping = service.stop_acquisition(
|
|
_stop_request(
|
|
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(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(_start_request(acquisition_id=acquisition_id))
|
|
|
|
with pytest.raises(RuntimeError, match="состояния starting"):
|
|
service.start_acquisition(_start_request(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(
|
|
_prepare_request(
|
|
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(_start_request(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(
|
|
_prepare_request(
|
|
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(_start_request(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(_abort_request(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(
|
|
_prepare_request(
|
|
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(_start_request(acquisition_id=acquisition_id))
|
|
control = FakeInteractiveControlSession()
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
_seed_supervised_connection(service)
|
|
service._device_session_id = device_session_id # noqa: SLF001
|
|
out_dir = service._acquisition_out_dir # noqa: SLF001
|
|
assert out_dir is not None
|
|
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(
|
|
_stop_request(
|
|
acquisition_id=acquisition_id,
|
|
mode="capture-only",
|
|
)
|
|
)
|
|
except BaseException as exc: # pragma: no cover - asserted below
|
|
worker_errors.append(exc)
|
|
finally:
|
|
stop_finished.set()
|
|
|
|
camera_snapshot = service.camera_preview.snapshot()
|
|
camera_snapshot.update(
|
|
{
|
|
"phase": "streaming",
|
|
"generation": 1,
|
|
"active_source_id": facade_module.DEFAULT_ACQUISITION_CAMERA_SOURCE,
|
|
"recording": {
|
|
"active": True,
|
|
"session": out_dir.name,
|
|
"active_epoch": 1,
|
|
"media_ready": True,
|
|
"current_epoch": {
|
|
"generation": 1,
|
|
"init_committed": True,
|
|
"first_media_committed": True,
|
|
},
|
|
},
|
|
}
|
|
)
|
|
monkeypatch.setattr(service, "_arm_camera_recording", blocked_camera_arm)
|
|
monkeypatch.setattr(
|
|
service.camera_preview,
|
|
"snapshot",
|
|
lambda: camera_snapshot,
|
|
)
|
|
monkeypatch.setattr(
|
|
service.camera_preview,
|
|
"select",
|
|
lambda source_id, _target: (
|
|
events.append("select")
|
|
or {
|
|
"active_source_id": source_id,
|
|
"recording": {"active": False},
|
|
}
|
|
),
|
|
)
|
|
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_receiver_start_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(
|
|
_prepare_request(
|
|
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,
|
|
recover_connection: object | None = None,
|
|
) -> None:
|
|
assert duration_seconds is None
|
|
assert project_name == PROJECT_NAME
|
|
assert recover_connection is None
|
|
events.append("start")
|
|
out_dir.mkdir(parents=True)
|
|
runtime.phase = "starting_live"
|
|
runtime.source_mode = "live"
|
|
raise RuntimeError("synthetic receiver start failure")
|
|
|
|
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)
|
|
camera_arm_calls: list[Path] = []
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_arm_camera_recording",
|
|
lambda out_dir, **_kwargs: camera_arm_calls.append(out_dir),
|
|
)
|
|
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 receiver start failure"):
|
|
service.start_acquisition(_start_request(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 camera_arm_calls == []
|
|
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(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
|
|
def fail_receiver_start(
|
|
_host: str,
|
|
out_dir: Path,
|
|
*,
|
|
duration_seconds: float | None,
|
|
project_name: str,
|
|
recover_connection: Callable[[int], str] | None = None,
|
|
) -> None:
|
|
assert duration_seconds is None
|
|
assert project_name == PROJECT_NAME
|
|
assert recover_connection is None
|
|
out_dir.mkdir()
|
|
raise RuntimeError("synthetic receiver start failure")
|
|
|
|
monkeypatch.setattr(runtime, "start_live", fail_receiver_start)
|
|
camera_arm_calls: list[Path] = []
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_arm_camera_recording",
|
|
lambda out_dir, **_kwargs: camera_arm_calls.append(out_dir),
|
|
)
|
|
monkeypatch.setattr(facade_module, "seal_capture_clock", lambda _capture_root: None)
|
|
runtime.stop_error = RuntimeError("synthetic cleanup timeout")
|
|
with pytest.raises(RuntimeError, match="synthetic receiver start failure"):
|
|
service.start_acquisition(_start_request(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
|
|
assert camera_arm_calls == []
|
|
|
|
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(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
duration_seconds=60,
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(_start_request(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(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
duration_seconds=60,
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(_start_request(acquisition_id=acquisition_id))
|
|
|
|
stopped = service.stop_acquisition(
|
|
_stop_request(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(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(_start_request(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(_stop_request(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(
|
|
_prepare_request(
|
|
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(
|
|
_prepare_request(
|
|
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(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(_start_request(acquisition_id=acquisition_id))
|
|
runtime.stop_error = RuntimeError("synthetic stop timeout")
|
|
with pytest.raises(RuntimeError, match="synthetic stop timeout"):
|
|
service.stop_acquisition(_stop_request(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(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(_start_request(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(_stop_request(acquisition_id=acquisition_id, mode="capture-only"))
|
|
|
|
runtime.stop_error = None
|
|
recovered = service.stop_acquisition(
|
|
_stop_request(
|
|
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(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(_start_request(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(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(_start_request(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(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
duration_seconds=60,
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(_start_request(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(
|
|
_stop_request(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(
|
|
_stop_request(
|
|
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(
|
|
_stop_request(
|
|
acquisition_id=acquisition_id,
|
|
mode="graceful",
|
|
operator_confirmed=True,
|
|
)
|
|
)
|
|
|
|
completed = service.stop_acquisition(
|
|
_stop_request(
|
|
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(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(_start_request(acquisition_id=acquisition_id))
|
|
runtime.mark_ready()
|
|
service.state()
|
|
runtime.pcl_frames = 1
|
|
service.state()
|
|
|
|
first = service.stop_acquisition(
|
|
_stop_request(
|
|
acquisition_id=acquisition_id,
|
|
idempotency_key="graceful-stop-once",
|
|
mode="graceful",
|
|
)
|
|
)
|
|
first_operation_id = first["last_operation"]["operation_id"]
|
|
retried = service.stop_acquisition(
|
|
_stop_request(
|
|
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(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(_start_request(acquisition_id=acquisition_id))
|
|
runtime.mark_ready()
|
|
service.state()
|
|
runtime.pcl_frames = 1
|
|
service.state()
|
|
first = service.stop_acquisition(_stop_request(acquisition_id=acquisition_id, mode="graceful"))
|
|
expected_operation_id = first["last_operation"]["operation_id"]
|
|
|
|
with pytest.raises(ValueError, match="уже ожидает"):
|
|
service.stop_acquisition(
|
|
_stop_request(
|
|
acquisition_id=acquisition_id,
|
|
idempotency_key="unrelated-stop-request",
|
|
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(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(_start_request(acquisition_id=acquisition_id))
|
|
|
|
with pytest.raises(ValueError, match="подтверждённого потока point cloud"):
|
|
service.stop_acquisition(_stop_request(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(
|
|
_prepare_request(
|
|
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(
|
|
_prepare_request(
|
|
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(
|
|
_prepare_request(
|
|
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(
|
|
_prepare_request(
|
|
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(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(_start_request(acquisition_id=acquisition_id))
|
|
runtime.mark_ready()
|
|
service.state()
|
|
runtime.pcl_frames = 1
|
|
service.state()
|
|
awaiting = service.stop_acquisition(
|
|
_stop_request(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(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(_start_request(acquisition_id=acquisition_id))
|
|
runtime.mark_ready()
|
|
service.state()
|
|
runtime.pcl_frames = 1
|
|
service.state()
|
|
awaiting = service.stop_acquisition(
|
|
_stop_request(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(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(_start_request(acquisition_id=acquisition_id))
|
|
runtime.pcl_frames = 1
|
|
service.state()
|
|
awaiting = service.stop_acquisition(
|
|
_stop_request(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(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(_start_request(acquisition_id=acquisition_id))
|
|
runtime.stop_error = RuntimeError("synthetic receiver stop failure")
|
|
|
|
with pytest.raises(RuntimeError, match="synthetic receiver stop failure"):
|
|
service.abort_acquisition(_abort_request(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(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(_start_request(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(_abort_request(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(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(_start_request(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(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(_start_request(acquisition_id=acquisition_id))
|
|
runtime.pcl_frames = 1
|
|
service.state()
|
|
awaiting = service.stop_acquisition(
|
|
_stop_request(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.")
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("cleanup_failure", [False, True], ids=["clean", "cleanup-failed"])
|
|
def test_close_settles_exact_prepared_stop_as_not_dispatched_after_worker_retirement(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
cleanup_failure: bool,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
monkeypatch.setattr(service.camera_preview, "close", lambda: None)
|
|
if cleanup_failure:
|
|
monkeypatch.setattr(
|
|
runtime,
|
|
"close",
|
|
lambda: (_ for _ in ()).throw(RuntimeError("synthetic shutdown cleanup failure")),
|
|
)
|
|
else:
|
|
monkeypatch.setattr(runtime, "close", runtime.stop)
|
|
|
|
if cleanup_failure:
|
|
with pytest.raises(RuntimeError, match="synthetic shutdown cleanup failure"):
|
|
service.close()
|
|
else:
|
|
service.close()
|
|
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.status == "failed"
|
|
assert operation.stage_code == "physical-stop-classified-not-dispatched"
|
|
assert operation.error is not None
|
|
assert operation.error["side_effect_status"] == "none"
|
|
assert operation.error["physical_command_sent"] is False
|
|
assert operation.error["automatic_replay_allowed"] is False
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None and record.stage == "prepared"
|
|
|
|
|
|
def test_close_never_classifies_stop_none_after_dispatch_boundary(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
dispatch = ApplicationMqttPublishEvidence(
|
|
operation_key="modeling:stop",
|
|
topic=fixture.envelope.topic,
|
|
payload_sha256=fixture.envelope.payload_sha256,
|
|
qos=2,
|
|
retain=False,
|
|
packet_id=None,
|
|
)
|
|
assert service._k1_command_dispatch_gate.acquire(blocking=False) # noqa: SLF001
|
|
try:
|
|
fixture.coordinator.publish_dispatching(dispatch)
|
|
finally:
|
|
service._k1_command_dispatch_gate.release() # noqa: SLF001
|
|
monkeypatch.setattr(service.camera_preview, "close", lambda: None)
|
|
monkeypatch.setattr(runtime, "close", runtime.stop)
|
|
service.close()
|
|
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None and record.stage == "dispatching"
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.status == "failed"
|
|
assert operation.stage_code == "physical-stop-outcome-unknown-local-retirement"
|
|
assert operation.error is not None
|
|
assert operation.error["side_effect_status"] == "unknown"
|
|
assert operation.error["automatic_replay_allowed"] is False
|
|
|
|
|
|
def test_close_with_live_prepared_worker_and_busy_dispatch_gate_stays_conservative(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_real_prepared_stop_dispatch_fixture(service, runtime)
|
|
service.stop_acquisition(fixture.request)
|
|
monkeypatch.setattr(service.camera_preview, "close", lambda: None)
|
|
monkeypatch.setattr(runtime, "close", runtime.stop)
|
|
|
|
assert service._k1_command_dispatch_gate.acquire(blocking=False) # noqa: SLF001
|
|
try:
|
|
with pytest.raises(
|
|
facade_module.LocalAcquisitionLifecycleError,
|
|
match="publish-переходом STOP",
|
|
) as raised:
|
|
service.close()
|
|
finally:
|
|
service._k1_command_dispatch_gate.release() # noqa: SLF001
|
|
|
|
assert raised.value.reason_code == "acquisition-stop-dispatch-retirement-pending"
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None and record.stage == "prepared"
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.status == "running"
|
|
assert operation.stage_code == "awaiting-external-stop"
|
|
assert operation.error is None
|
|
assert service._acquisition_stop_operation_id == fixture.stop_operation_id # noqa: SLF001
|
|
assert service._acquisition is not None # noqa: SLF001
|
|
assert service._acquisition.state == "awaiting_external_stop" # noqa: SLF001
|
|
assert runtime.stop_calls == 0
|
|
|
|
|
|
def test_close_defers_real_worker_queued_before_dispatch_without_tearing_owner(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
fixture = _install_actual_worker_recovered_stop_fixture(
|
|
service,
|
|
runtime,
|
|
monkeypatch,
|
|
)
|
|
service.stop_acquisition(fixture.request)
|
|
assert fixture.transport.before_transport_guard.wait(3.0)
|
|
cleanup_calls: list[str] = []
|
|
monkeypatch.setattr(
|
|
service.camera_preview,
|
|
"close",
|
|
lambda: cleanup_calls.append("camera"),
|
|
)
|
|
monkeypatch.setattr(runtime, "close", lambda: cleanup_calls.append("runtime"))
|
|
|
|
with pytest.raises(
|
|
facade_module.LocalAcquisitionLifecycleError,
|
|
match="не завершил PREPARED STOP",
|
|
) as raised:
|
|
service.close()
|
|
|
|
assert raised.value.reason_code == "acquisition-stop-worker-retirement-pending"
|
|
assert cleanup_calls == []
|
|
assert fixture.transport.fake_client_publish_calls == []
|
|
assert fixture.transport.publish_attempts == 0
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.status == "running"
|
|
assert operation.error is None
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None and record.stage == "prepared"
|
|
assert service._acquisition_stop_operation_id == fixture.stop_operation_id # noqa: SLF001
|
|
assert service._acquisition is not None # noqa: SLF001
|
|
assert service._acquisition.state == "failed" # noqa: SLF001
|
|
assert service._acquisition.result is not None # noqa: SLF001
|
|
assert service._acquisition.result["recovery_only"] is True # noqa: SLF001
|
|
assert runtime.stop_calls == 0
|
|
|
|
# The shutdown attempt explicitly failed, so the still-owned operation may
|
|
# resume. Its eventual publish is retained as ambiguous evidence, never
|
|
# retroactively labelled cancelled or not-dispatched.
|
|
fixture.transport.release_fake_publish.set()
|
|
fixture.transport.release_transport_guard.set()
|
|
closed = _wait_control_state(fixture.control, {"closed"})
|
|
assert closed["state"] == "closed"
|
|
assert len(fixture.transport.fake_client_publish_calls) == 1
|
|
record = fixture.ledger.snapshot().record
|
|
assert record is not None and record.stage in {"dispatching", "observing"}
|
|
operation = service._operations.get(fixture.stop_operation_id) # noqa: SLF001
|
|
assert operation.status == "running"
|
|
assert operation.stage_code != "physical-stop-classified-not-dispatched"
|
|
|
|
|
|
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(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(_start_request(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(
|
|
_prepare_request(
|
|
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(
|
|
_prepare_request(
|
|
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)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
_reachable_control_endpoint,
|
|
)
|
|
_set_scanned_devices(
|
|
service,
|
|
[{"device_id": "k1-a"}, {"device_id": "k1-b"}],
|
|
)
|
|
boundary_calls: list[tuple[str, 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,
|
|
*,
|
|
write_mode: str,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**_: object,
|
|
) -> dict[str, Any]:
|
|
boundary_calls.append((device_id, ssid, password, write_mode))
|
|
_dispatch_test_network_write(on_write_dispatch)
|
|
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": _wifi_status_read("192.168.1.20")["status"]}],
|
|
}
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", fake_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, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_control_endpoint_reachable",
|
|
lambda _target: True,
|
|
)
|
|
first = asyncio.create_task(
|
|
service.connect(
|
|
_connect_request(
|
|
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(facade_module.ProvisioningAlreadyRunning):
|
|
await service.connect(
|
|
_connect_request(
|
|
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, "with_response")]
|
|
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 len(provision_operations) == 1
|
|
assert provision_operations[0]["status"] == "succeeded"
|
|
attempt = connected["connection_attempt"]
|
|
assert attempt["attempt_id"] == provision_operations[0]["operation_id"]
|
|
assert attempt["connection_mode"] == "bridge"
|
|
assert attempt["side_effect_status"] == "applied"
|
|
assert attempt["safe_next_action"] == "start-acquisition"
|
|
assert [event["stage_code"] for event in attempt["timeline"]] == [
|
|
"accepted",
|
|
"scan-selection-admitted",
|
|
"ble-provisioning-write",
|
|
"ble-write-dispatched",
|
|
"status-observing",
|
|
"device-topology-applied",
|
|
"host-wifi-switch-not-authorized",
|
|
"network-configured",
|
|
"accepted",
|
|
"host-route-and-control-endpoint",
|
|
"device-info-confirmed",
|
|
]
|
|
diagnostic_bundle = attempt["diagnostic_bundle"]
|
|
assert diagnostic_bundle["redacted"] is True
|
|
assert diagnostic_bundle["automatic_retry"] is False
|
|
assert PRIMARY_TEST_CREDENTIAL not in json.dumps(diagnostic_bundle)
|
|
assert "lab-network" not in json.dumps(diagnostic_bundle)
|
|
|
|
switched = service.select_connection_mode(
|
|
DesiredConnectionModeRequest(
|
|
connection_mode="quick-connect",
|
|
expected_revision=connected["desired_connection_mode_revision"],
|
|
)
|
|
)
|
|
assert switched["connection_lifecycle"]["ready_to_start"] is False
|
|
assert switched["connection_attempt"]["status"] == "succeeded"
|
|
assert switched["connection_attempt"]["safe_next_action"] == "scan-select-connect"
|
|
assert (
|
|
switched["connection_attempt"]["diagnostic_bundle"]["attempt"]["safe_next_action"]
|
|
== "scan-select-connect"
|
|
)
|
|
|
|
cancelled = service.select_connection_mode(
|
|
DesiredConnectionModeRequest(
|
|
connection_mode="bridge",
|
|
expected_revision=switched["desired_connection_mode_revision"],
|
|
)
|
|
)
|
|
assert cancelled["connection_lifecycle"]["ready_to_start"] is True
|
|
assert cancelled["connection_attempt"]["safe_next_action"] == "start-acquisition"
|
|
assert (
|
|
cancelled["connection_attempt"]["diagnostic_bundle"]["attempt"]["safe_next_action"]
|
|
== "start-acquisition"
|
|
)
|
|
|
|
invalidations: list[bool] = []
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"invalidate_connected_device_session",
|
|
lambda *_args, **_kwargs: invalidations.append(True) or True,
|
|
)
|
|
lost_path = HostPathProbeResult(
|
|
available=False,
|
|
fingerprint=None,
|
|
interface=None,
|
|
source_ipv4=None,
|
|
route_class="unavailable",
|
|
reason_code="router-link-lost",
|
|
)
|
|
disconnected = cancelled
|
|
for _ in range(3):
|
|
service._connection_supervisor.observe_host_path(lost_path) # noqa: SLF001
|
|
disconnected = service.state()
|
|
|
|
assert invalidations == [True]
|
|
assert disconnected["connection_lifecycle"]["active_mode"] is None
|
|
assert disconnected["connection_lifecycle"]["ready_to_start"] is False
|
|
assert disconnected["connection_attempt"]["status"] == "succeeded"
|
|
assert disconnected["connection_attempt"]["safe_next_action"] == ("scan-select-connect")
|
|
assert (
|
|
disconnected["connection_attempt"]["diagnostic_bundle"]["attempt"]["safe_next_action"]
|
|
== "scan-select-connect"
|
|
)
|
|
|
|
|
|
def test_replay_cannot_start_while_network_transition_owns_lifecycle_gate(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
|
|
async def scenario() -> None:
|
|
entered = asyncio.Event()
|
|
release = asyncio.Event()
|
|
|
|
async def held_network_write(
|
|
*_args: object,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**_kwargs: object,
|
|
) -> dict[str, Any]:
|
|
_dispatch_test_network_write(on_write_dispatch)
|
|
entered.set()
|
|
await release.wait()
|
|
return {
|
|
"started_at_utc": "2026-08-08T12:00:00Z",
|
|
"completed_at_utc": "2026-08-08T12:00:01Z",
|
|
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
|
"outcome": "lan_address_observed",
|
|
"write_mode": "with_response",
|
|
"observations": [
|
|
{"status": _wifi_status_read("192.168.68.50", device_id="k1-a")["status"]}
|
|
],
|
|
}
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", held_network_write)
|
|
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, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", lambda _target: True)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
_reachable_control_endpoint,
|
|
)
|
|
network_task = asyncio.create_task(
|
|
service.connect(
|
|
_connect_request(
|
|
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(facade_module.ApplicationControlProcessLeaseUnavailable):
|
|
await asyncio.to_thread(
|
|
service.start_replay,
|
|
"sessions/fixture.k1mqtt",
|
|
1.0,
|
|
False,
|
|
)
|
|
assert runtime.phase == "idle"
|
|
assert runtime.source_mode == "idle"
|
|
release.set()
|
|
await asyncio.wait_for(network_task, timeout=1.0)
|
|
|
|
asyncio.run(scenario())
|
|
|
|
|
|
def test_network_transition_cannot_enter_while_replay_start_owns_lifecycle_gate(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
replay_path = service.repository_root / "sessions" / "fixture.k1mqtt"
|
|
replay_path.parent.mkdir(parents=True, exist_ok=True)
|
|
replay_path.write_bytes(b"fixture")
|
|
entered = threading.Event()
|
|
release = threading.Event()
|
|
|
|
def held_replay_start(_path: Path, *, speed: float, loop: bool) -> None:
|
|
assert speed == 1.0
|
|
assert loop is False
|
|
runtime.phase = "replay"
|
|
runtime.source_mode = "replay"
|
|
entered.set()
|
|
assert release.wait(timeout=1.0)
|
|
|
|
monkeypatch.setattr(runtime, "start_replay", held_replay_start, raising=False)
|
|
replay_errors: list[BaseException] = []
|
|
|
|
def replay_worker() -> None:
|
|
try:
|
|
service.start_replay(str(replay_path), speed=1.0, loop=False)
|
|
except BaseException as exc: # pragma: no cover - assertion captures it below
|
|
replay_errors.append(exc)
|
|
|
|
thread = threading.Thread(target=replay_worker, daemon=True)
|
|
thread.start()
|
|
assert entered.wait(timeout=1.0)
|
|
try:
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as raised:
|
|
asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
assert raised.value.reason_code == "network-provision-lifecycle-busy"
|
|
finally:
|
|
release.set()
|
|
thread.join(timeout=1.0)
|
|
|
|
assert not thread.is_alive()
|
|
assert replay_errors == []
|
|
assert runtime.phase == "replay"
|
|
assert runtime.source_mode == "replay"
|
|
|
|
|
|
def test_exact_network_idempotency_replay_precedes_expired_candidate_checks(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_devices(service, [{"device_id": "k1-a"}])
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
_reachable_control_endpoint,
|
|
)
|
|
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, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_control_endpoint_reachable",
|
|
lambda _target: True,
|
|
)
|
|
writes = 0
|
|
|
|
async def fake_provision(
|
|
*_: object,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**__: object,
|
|
) -> dict[str, Any]:
|
|
nonlocal writes
|
|
writes += 1
|
|
_dispatch_test_network_write(on_write_dispatch)
|
|
return {
|
|
"started_at_utc": "2026-08-06T08:00:00Z",
|
|
"completed_at_utc": "2026-08-06T08:00:01Z",
|
|
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
|
"outcome": "lan_address_observed",
|
|
"write_mode": "with_response",
|
|
"observations": [{"status": _wifi_status_read("192.168.1.20")["status"]}],
|
|
}
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", fake_provision)
|
|
request = _connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
idempotency_key="one-browser-intent",
|
|
)
|
|
asyncio.run(service.connect(request))
|
|
journal_path = service._require_network_provisioning_idempotency_journal().path # noqa: SLF001
|
|
journal_document = journal_path.read_text(encoding="utf-8")
|
|
assert "one-browser-intent" not in journal_document
|
|
assert "lab-network" not in journal_document
|
|
assert PRIMARY_TEST_CREDENTIAL not in journal_document
|
|
service._devices = [] # noqa: SLF001
|
|
service._ble_device_last_seen_monotonic = {} # noqa: SLF001
|
|
service._ble_device_last_seen_suspend_aware = {} # noqa: SLF001
|
|
|
|
replayed = asyncio.run(service.connect(request))
|
|
|
|
restarted_service, _ = service_with_fake_runtime(tmp_path)
|
|
restarted = asyncio.run(restarted_service.connect(request))
|
|
|
|
with pytest.raises(NetworkProvisioningIdempotencyConflict):
|
|
asyncio.run(
|
|
restarted_service.connect(request.model_copy(update={"ssid": "different-network"}))
|
|
)
|
|
|
|
with pytest.raises(NetworkProvisioningIdempotencyConflict):
|
|
asyncio.run(
|
|
restarted_service.connect(request.model_copy(update={"allow_host_wifi_switch": True}))
|
|
)
|
|
|
|
assert writes == 1
|
|
operations = [item for item in replayed["operations"] if item["action"] == "network.provision"]
|
|
assert len(operations) == 1
|
|
assert operations[0]["status"] == "succeeded"
|
|
restarted_operation = next(
|
|
item for item in restarted["operations"] if item["action"] == "network.provision"
|
|
)
|
|
assert restarted_operation["operation_id"] == operations[0]["operation_id"]
|
|
assert restarted_operation["status"] == "succeeded"
|
|
assert restarted_operation["stage_code"] == "durable-terminal-replay"
|
|
assert restarted_operation["result"]["phase"] == "network_applied"
|
|
assert restarted_operation["result"]["control_state"] == "unknown"
|
|
assert restarted_operation["result"]["replay_binding_available"] is True
|
|
assert restarted_operation["result"]["snapshot_runtime_id"] == restarted["snapshot_runtime_id"]
|
|
assert restarted_operation["result"]["parent_intent_id"] == restarted_operation["operation_id"]
|
|
assert restarted_operation["result"]["transport_ref"] == "k1-a"
|
|
assert restarted_operation["result"]["connection_mode"] == "bridge"
|
|
assert restarted_operation["result"]["target_ipv4"] == "192.168.1.20"
|
|
assert restarted["connection_attempt"]["phase"] == "network_applied"
|
|
assert restarted["connection_attempt"]["control_state"] == "unknown"
|
|
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_probe_control_endpoint_socket",
|
|
lambda _target: facade_module.TcpReachabilityProbeResult(reachable=True),
|
|
)
|
|
verified = asyncio.run(restarted_service.verify_connection(ConnectionVerifyRequest()))
|
|
|
|
assert writes == 1
|
|
assert verified["last_operation"]["action"] == "connection.verify"
|
|
assert verified["last_operation"]["status"] == "succeeded"
|
|
assert verified["connection_attempt"]["attempt_id"] == restarted_operation["operation_id"]
|
|
assert verified["connection_attempt"]["phase"] == "network_applied"
|
|
assert verified["connection_attempt"]["control_state"] == "ready"
|
|
assert verified["connection_attempt"]["safe_next_action"] == "start-acquisition"
|
|
|
|
|
|
def test_successful_session_requires_new_scan_generation_before_next_connect(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
targets = iter(["192.168.68.50", "192.168.68.51"])
|
|
writes = 0
|
|
|
|
async def successful_write(
|
|
*_args: object,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**_kwargs: object,
|
|
) -> dict[str, Any]:
|
|
nonlocal writes
|
|
writes += 1
|
|
target = next(targets)
|
|
_dispatch_test_network_write(on_write_dispatch)
|
|
return {
|
|
"started_at_utc": "2026-08-08T13:00:00Z",
|
|
"completed_at_utc": "2026-08-08T13:00:01Z",
|
|
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
|
"outcome": "lan_address_observed",
|
|
"write_mode": "with_response",
|
|
"observations": [{"status": _wifi_status_read(target, device_id="k1-a")["status"]}],
|
|
}
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", successful_write)
|
|
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, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", lambda _target: True)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
_reachable_control_endpoint,
|
|
)
|
|
first = asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
first_session = first["device_session"]["device_session_id"]
|
|
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as raised:
|
|
asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
assert raised.value.reason_code == "network-provision-candidate-not-fresh"
|
|
assert service._device_session_id == first_session # noqa: SLF001
|
|
assert service._selected_device_id == "k1-a" # noqa: SLF001
|
|
assert writes == 1
|
|
|
|
service._ble_discovery_generation += 1 # noqa: SLF001
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
with pytest.raises(RuntimeError, match="не сообщило адрес"):
|
|
asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_discovery_generation=1,
|
|
)
|
|
)
|
|
)
|
|
assert service._device_session_id is None # noqa: SLF001
|
|
assert service._selected_device_id is None # noqa: SLF001
|
|
assert writes == 2
|
|
|
|
|
|
def test_operation_admission_failure_terminalizes_prepared_network_intent(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
|
|
def fail_operation_admission(*_args: object, **_kwargs: object) -> object:
|
|
raise RuntimeError("synthetic operation journal failure")
|
|
|
|
monkeypatch.setattr(service._operations, "begin", fail_operation_admission) # noqa: SLF001
|
|
request = _connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
idempotency_key="operation-admission-failure",
|
|
)
|
|
|
|
with pytest.raises(RuntimeError, match="operation journal failure"):
|
|
asyncio.run(service.connect(request))
|
|
|
|
snapshot = service._require_network_provisioning_idempotency_journal().snapshot() # noqa: SLF001
|
|
assert snapshot.active_record is None
|
|
assert len(snapshot.records) == 1
|
|
record = snapshot.records[0]
|
|
assert record.stage == "terminal"
|
|
assert record.terminal is not None
|
|
assert record.terminal.outcome == "failed"
|
|
assert record.terminal.outcome_code == "network.provision.admission_failed"
|
|
assert record.terminal.error_code == "operation-journal-admission-failed"
|
|
assert record.terminal.side_effect_status == "none"
|
|
assert record.terminal.safe_to_retry is True
|
|
|
|
|
|
def test_live_process_lease_prevents_false_restart_recovery_of_prepared_request(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
request = _connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
idempotency_key="live-owner-prepared-request",
|
|
)
|
|
canonical_request = json.dumps(
|
|
{
|
|
"schema_version": "missioncore.xgrids-k1-network-provision-request/v1",
|
|
"device_id": request.device_id,
|
|
"ssid": request.ssid,
|
|
"password": PRIMARY_TEST_CREDENTIAL,
|
|
"connection_mode": request.connection_mode,
|
|
"expected_mode_revision": request.expected_mode_revision,
|
|
"expected_discovery_generation": request.expected_discovery_generation,
|
|
"expected_reconfiguration_revision": (request.expected_reconfiguration_revision),
|
|
"expected_reconfiguration_intent_id": (request.expected_reconfiguration_intent_id),
|
|
"compatibility_attestation": request.compatibility_attestation.model_dump(mode="json"),
|
|
},
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
).encode("utf-8")
|
|
owner_lease = ApplicationControlProcessLease.acquire(tmp_path)
|
|
try:
|
|
journal = service._require_network_provisioning_idempotency_journal() # noqa: SLF001
|
|
operation_id = "op-11111111-2222-4333-8444-555555555555"
|
|
admitted = journal.begin(
|
|
idempotency_key=request.idempotency_key,
|
|
action=facade_module.ACTION_NETWORK_PROVISION,
|
|
operation_id=operation_id,
|
|
request_binding_sha256=derive_request_binding_sha256(
|
|
request.idempotency_key,
|
|
action=facade_module.ACTION_NETWORK_PROVISION,
|
|
canonical_request=canonical_request,
|
|
),
|
|
)
|
|
assert admitted.record.stage == "prepared"
|
|
|
|
competing_service, _ = service_with_fake_runtime(tmp_path)
|
|
still_live = journal.snapshot().active_record
|
|
assert still_live is not None
|
|
assert still_live.operation_id == operation_id
|
|
assert still_live.stage == "prepared"
|
|
finally:
|
|
owner_lease.release()
|
|
|
|
# The already-running competing facade may take over only after the OS
|
|
# lease proves that the original process is gone. It replays a durable,
|
|
# no-side-effect terminal result without touching BLE.
|
|
replayed = asyncio.run(competing_service.connect(request))
|
|
terminal = journal.snapshot().records[-1]
|
|
operation = next(
|
|
item for item in replayed["operations"] if item["action"] == "network.provision"
|
|
)
|
|
assert terminal.stage == "terminal"
|
|
assert terminal.terminal is not None
|
|
assert terminal.terminal.side_effect_status == "none"
|
|
assert terminal.terminal.safe_to_retry is True
|
|
assert operation["operation_id"] == operation_id
|
|
assert operation["status"] == "failed"
|
|
assert operation["stage_code"] == "durable-terminal-replay"
|
|
|
|
|
|
@pytest.mark.parametrize("legacy_stage", ["dispatching", "observing"])
|
|
def test_restart_terminalizes_dispatched_legacy_ledger_as_interrupted_audit(
|
|
tmp_path: Path,
|
|
legacy_stage: str,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
operation_id = f"legacy-{legacy_stage}-operation"
|
|
_seed_legacy_network_mutation_without_idempotency(
|
|
service,
|
|
operation_id=operation_id,
|
|
stage=legacy_stage,
|
|
)
|
|
journal_path = service._require_network_provisioning_idempotency_journal().path # noqa: SLF001
|
|
assert not journal_path.exists()
|
|
|
|
restarted, _ = service_with_fake_runtime(tmp_path)
|
|
idempotency = restarted._require_network_provisioning_idempotency_journal().snapshot() # noqa: SLF001
|
|
|
|
assert idempotency.status == "ready"
|
|
assert idempotency.active_record is None
|
|
terminal = idempotency.records[-1]
|
|
assert terminal.operation_id == operation_id
|
|
assert terminal.action == facade_module.ACTION_NETWORK_PROVISION
|
|
assert terminal.stage == "terminal"
|
|
assert terminal.terminal is not None
|
|
assert terminal.terminal.outcome_code == "network.provision.interrupted"
|
|
assert terminal.terminal.side_effect_status == "reconciled"
|
|
assert terminal.terminal.safe_to_retry is False
|
|
persisted = journal_path.read_text(encoding="utf-8")
|
|
assert "test-ble-transport" not in persisted
|
|
assert "WIFI_CLIENT" not in persisted
|
|
assert '"stage":"terminal"' in persisted
|
|
state = restarted.state()
|
|
assert state["network_mutation_ledger"]["stage"] == "resolved"
|
|
assert state["network_mutation_ledger"]["resolution"] == "interrupted"
|
|
assert state["network_write_reconciliation"] is None
|
|
|
|
|
|
def test_restart_resolves_legacy_prepared_as_not_dispatched_without_adoption(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_legacy_network_mutation_without_idempotency(
|
|
service,
|
|
operation_id="legacy-prepared-operation",
|
|
stage="prepared",
|
|
)
|
|
journal_path = service._require_network_provisioning_idempotency_journal().path # noqa: SLF001
|
|
assert not journal_path.exists()
|
|
|
|
restarted, _ = service_with_fake_runtime(tmp_path)
|
|
state = restarted.state()
|
|
|
|
assert state["network_mutation_ledger"]["status"] == "resolved"
|
|
assert state["network_mutation_ledger"]["resolution"] == "not-dispatched"
|
|
assert state["network_write_reconciliation"] is None
|
|
assert state["network_provisioning_idempotency"]["status"] == "empty"
|
|
assert state["network_provisioning_idempotency"]["active_operation_id"] is None
|
|
assert not journal_path.exists()
|
|
|
|
|
|
def test_restart_supersedes_cross_journal_mismatch_without_blocking_new_intent(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_unresolved_network_mutation(
|
|
service,
|
|
operation_id="legacy-ledger-operation",
|
|
idempotency_operation_id="other-idempotency-operation",
|
|
)
|
|
|
|
restarted, _ = service_with_fake_runtime(tmp_path)
|
|
state = restarted.state()
|
|
|
|
assert state["network_mutation_ledger"]["status"] == "resolved"
|
|
assert state["network_mutation_ledger"]["operation_id"] == "legacy-ledger-operation"
|
|
assert state["network_mutation_ledger"]["resolution"] == "interrupted"
|
|
assert state["network_provisioning_idempotency"]["status"] == "ready"
|
|
assert state["network_provisioning_idempotency"]["reason_code"] is None
|
|
assert state["network_provisioning_idempotency"]["mutation_allowed"] is True
|
|
|
|
|
|
def test_new_explicit_intent_is_admitted_after_interrupted_dispatched_session(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_unresolved_network_mutation(
|
|
service,
|
|
operation_id="interrupted-operation",
|
|
idempotency_operation_id="interrupted-operation",
|
|
)
|
|
journal = service._require_network_provisioning_idempotency_journal() # noqa: SLF001
|
|
|
|
recovered = facade_module._recover_network_provisioning_under_exclusive_process_lease( # noqa: SLF001
|
|
network_ledger=service._network_mutation_ledger, # noqa: SLF001
|
|
idempotency_journal=journal,
|
|
)
|
|
admitted = journal.begin(
|
|
idempotency_key="new-explicit-operator-intent",
|
|
action=facade_module.ACTION_NETWORK_PROVISION,
|
|
operation_id="new-explicit-operation",
|
|
request_binding_sha256="a" * 64,
|
|
)
|
|
|
|
assert recovered.status == "resolved"
|
|
assert recovered.record is not None
|
|
assert recovered.record.resolution == "interrupted"
|
|
assert admitted.disposition == "admitted"
|
|
assert admitted.record.stage == "prepared"
|
|
|
|
|
|
def test_restart_legacy_adoption_preserves_corrupt_journal_and_fails_closed(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_legacy_network_mutation_without_idempotency(service)
|
|
journal_path = service._require_network_provisioning_idempotency_journal().path # noqa: SLF001
|
|
corrupt = b'{"schema_version":"corrupt","records":[]}\n'
|
|
journal_path.write_bytes(corrupt)
|
|
journal_path.chmod(0o600)
|
|
|
|
restarted, _ = service_with_fake_runtime(tmp_path)
|
|
state = restarted.state()
|
|
|
|
assert journal_path.read_bytes() == corrupt
|
|
assert state["network_mutation_ledger"]["status"] == "unresolved"
|
|
assert state["network_provisioning_idempotency"]["status"] == "corrupt"
|
|
assert state["network_provisioning_idempotency"]["reason_code"] == (
|
|
"network-provisioning-idempotency-corrupt"
|
|
)
|
|
assert state["network_provisioning_idempotency"]["mutation_allowed"] is False
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"corruption",
|
|
[
|
|
"wrong-mode",
|
|
"oversize",
|
|
"symlink",
|
|
"ledger-wrong-mode",
|
|
"both-corrupt",
|
|
"journal-disappeared",
|
|
],
|
|
)
|
|
def test_explicit_connect_quarantines_corrupt_network_audit_without_replay(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
corruption: str,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
journal = service._require_network_provisioning_idempotency_journal() # noqa: SLF001
|
|
path = journal.path
|
|
ledger_path = service._network_mutation_ledger.path # noqa: SLF001
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
foreign_target = tmp_path / "foreign-audit-target"
|
|
if corruption in {"wrong-mode", "both-corrupt", "journal-disappeared"}:
|
|
path.write_text('{"schema_version":"wrong"}\n', encoding="utf-8")
|
|
path.chmod(0o644)
|
|
elif corruption == "oversize":
|
|
path.write_bytes(b"x" * (300 * 1024))
|
|
path.chmod(0o600)
|
|
else:
|
|
if corruption == "symlink":
|
|
foreign_target.write_text("foreign", encoding="utf-8")
|
|
path.unlink(missing_ok=True)
|
|
path.symlink_to(foreign_target)
|
|
if corruption in {"ledger-wrong-mode", "both-corrupt"}:
|
|
ledger_path.write_text('{"schema_version":"wrong"}\n', encoding="utf-8")
|
|
ledger_path.chmod(0o644)
|
|
if corruption == "journal-disappeared":
|
|
service._network_provisioning_idempotency_journal = None # noqa: SLF001
|
|
service._network_provisioning_idempotency_reason = ( # noqa: SLF001
|
|
"network-provisioning-idempotency-corrupt"
|
|
)
|
|
path.unlink()
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
|
|
async def successful_write(
|
|
*_args: object,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**_kwargs: object,
|
|
) -> dict[str, Any]:
|
|
_dispatch_test_network_write(on_write_dispatch)
|
|
return {
|
|
"started_at_utc": "2026-08-08T10:00:00Z",
|
|
"completed_at_utc": "2026-08-08T10:00:01Z",
|
|
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
|
"outcome": "lan_address_observed",
|
|
"write_mode": "with_response",
|
|
"baseline_status": _wifi_status_read(None, device_id="k1-a")["status"],
|
|
"observations": [
|
|
{"status": _wifi_status_read("192.168.68.50", device_id="k1-a")["status"]}
|
|
],
|
|
}
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", successful_write)
|
|
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, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", lambda _target: True)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
_reachable_control_endpoint,
|
|
)
|
|
|
|
state = asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert state["connection_mode"] == "bridge"
|
|
assert state["network_provisioning_idempotency"]["status"] == "ready"
|
|
journal_quarantines = list(path.parent.glob(f"{path.name}.corrupt-*"))
|
|
ledger_quarantines = list(ledger_path.parent.glob(f"{ledger_path.name}.corrupt-*"))
|
|
assert len(journal_quarantines) == (
|
|
1
|
|
if corruption
|
|
in {
|
|
"wrong-mode",
|
|
"oversize",
|
|
"symlink",
|
|
"both-corrupt",
|
|
}
|
|
else 0
|
|
)
|
|
assert len(ledger_quarantines) == (
|
|
1
|
|
if corruption
|
|
in {
|
|
"ledger-wrong-mode",
|
|
"both-corrupt",
|
|
}
|
|
else 0
|
|
)
|
|
if corruption == "symlink":
|
|
assert foreign_target.read_text(encoding="utf-8") == "foreign"
|
|
|
|
|
|
def test_cancelled_bridge_write_is_audited_and_new_explicit_intent_writes_once(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_devices(service, [{"device_id": "k1-a"}])
|
|
calls = 0
|
|
|
|
async def scenario() -> None:
|
|
entered = asyncio.Event()
|
|
|
|
async def cancelled_write(
|
|
*_: object,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**__: object,
|
|
) -> dict[str, Any]:
|
|
nonlocal calls
|
|
calls += 1
|
|
assert on_write_dispatch is not None
|
|
on_write_dispatch(
|
|
_wifi_status_read(None, device_id="k1-a")["status"],
|
|
"with_response",
|
|
)
|
|
entered.set()
|
|
try:
|
|
await asyncio.Event().wait()
|
|
except asyncio.CancelledError as exc:
|
|
exc.operation_stage = "gatt-write" # type: ignore[attr-defined]
|
|
exc.device_write_attempted = True # type: ignore[attr-defined]
|
|
exc.device_write_confirmed = True # type: ignore[attr-defined]
|
|
raise
|
|
|
|
async def successful_second_write(
|
|
*_: object,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**__: object,
|
|
) -> dict[str, Any]:
|
|
nonlocal calls
|
|
calls += 1
|
|
_dispatch_test_network_write(on_write_dispatch)
|
|
return {
|
|
"started_at_utc": "2026-08-08T10:00:00Z",
|
|
"completed_at_utc": "2026-08-08T10:00:01Z",
|
|
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
|
"outcome": "lan_address_observed",
|
|
"write_mode": "with_response",
|
|
"baseline_status": _wifi_status_read(None, device_id="k1-a")["status"],
|
|
"observations": [
|
|
{"status": _wifi_status_read("192.168.68.50", device_id="k1-a")["status"]}
|
|
],
|
|
}
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", cancelled_write)
|
|
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, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", lambda _target: True)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
_reachable_control_endpoint,
|
|
)
|
|
first = asyncio.create_task(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
idempotency_key="cancelled-write-1",
|
|
)
|
|
)
|
|
)
|
|
await entered.wait()
|
|
first.cancel()
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await first
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", successful_second_write)
|
|
second = await service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
idempotency_key="cancelled-write-2",
|
|
)
|
|
)
|
|
assert second["connection_mode"] == "bridge"
|
|
assert second["k1_ip"] == "192.168.68.50"
|
|
|
|
asyncio.run(scenario())
|
|
|
|
state = service.state()
|
|
operation = next(
|
|
item for item in state["operations"] if item["idempotency_key"] == "cancelled-write-1"
|
|
)
|
|
assert operation["status"] == "failed"
|
|
assert operation["error"]["code"] == "CancelledError"
|
|
assert operation["error"]["side_effect_status"] == "unknown"
|
|
assert operation["error"]["safe_to_retry"] is False
|
|
assert state["network_mutation_ledger"]["status"] == "resolved"
|
|
assert state["network_write_reconciliation"] is None
|
|
assert calls == 2
|
|
|
|
|
|
def test_cancelled_host_mutation_finishes_before_ownership_is_released() -> None:
|
|
started = threading.Event()
|
|
release = threading.Event()
|
|
completed = threading.Event()
|
|
|
|
def bounded_host_mutation() -> str:
|
|
started.set()
|
|
assert release.wait(timeout=2.0)
|
|
completed.set()
|
|
return "completed"
|
|
|
|
async def scenario() -> None:
|
|
task = asyncio.create_task(
|
|
facade_module._run_blocking_operation_without_abandonment( # noqa: SLF001
|
|
bounded_host_mutation
|
|
)
|
|
)
|
|
for _ in range(100):
|
|
if started.is_set():
|
|
break
|
|
await asyncio.sleep(0.01)
|
|
assert started.is_set()
|
|
task.cancel()
|
|
await asyncio.sleep(0.01)
|
|
assert not task.done()
|
|
task.cancel()
|
|
await asyncio.sleep(0.01)
|
|
assert not task.done()
|
|
release.set()
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await task
|
|
assert completed.is_set()
|
|
|
|
asyncio.run(scenario())
|
|
|
|
|
|
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)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
_reachable_control_endpoint,
|
|
)
|
|
_set_scanned_devices(service, [{"device_id": "k1-a", "name": "XGR-TEST-A"}])
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
service.abort_acquisition(
|
|
_abort_request(
|
|
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,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**__: object,
|
|
) -> dict[str, Any]:
|
|
_dispatch_test_network_write(on_write_dispatch)
|
|
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": _wifi_status_read("192.168.1.20")["status"]}],
|
|
}
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", fake_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, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_control_endpoint_reachable",
|
|
lambda _target: True,
|
|
)
|
|
connected = asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
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 connected["device_session"]["connectivity"] == "connected"
|
|
assert connected["connection_verification"]["lease_state"] == "reachable"
|
|
assert connected["connection_lifecycle"]["connection_ready"] is True
|
|
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"}])
|
|
host_associations: list[tuple[str, str]] = []
|
|
|
|
async def fake_provision(
|
|
*_: object,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**__: object,
|
|
) -> dict[str, Any]:
|
|
_dispatch_test_network_write(on_write_dispatch)
|
|
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": _wifi_status_read("192.168.68.50")["status"]}],
|
|
}
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", fake_provision)
|
|
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
|
|
monkeypatch.setattr(facade_module, "_host_route_class", lambda _target: "tunnel")
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _tunnel_host_path)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
_unreachable_control_endpoint,
|
|
)
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", lambda _target: False)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"associate_with_ephemeral_wifi_credentials_once",
|
|
lambda _helper, ssid, password, **_kwargs: (
|
|
host_associations.append((ssid, password))
|
|
or {
|
|
"adapter": "CoreWLAN",
|
|
"outcome": "associated",
|
|
"already_associated": False,
|
|
"scan_attempt_count": 1,
|
|
"scan_elapsed_ms": 20,
|
|
}
|
|
),
|
|
)
|
|
|
|
async def connect_and_join_read_only_continuation() -> dict[str, Any]:
|
|
await service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
connection_mode="bridge",
|
|
compatibility_attestation=ATTESTATION,
|
|
allow_host_wifi_switch=True,
|
|
)
|
|
)
|
|
with service._lock: # noqa: SLF001
|
|
task = service._control_bootstrap_continuation_task # noqa: SLF001
|
|
if task is not None:
|
|
with pytest.raises(facade_module.ConnectionVerificationError):
|
|
await task
|
|
return service.state()
|
|
|
|
connected = asyncio.run(connect_and_join_read_only_continuation())
|
|
|
|
assert host_associations == [("lab-router", PRIMARY_TEST_CREDENTIAL)]
|
|
assert connected["k1_ip"] == "192.168.68.50"
|
|
assert connected["phase"] == "device_selected"
|
|
assert connected["device_session"]["connectivity"] == "offline"
|
|
assert connected["connection_verification"] == {
|
|
"status": "endpoint-unreachable",
|
|
"lease_state": "configured-unverified",
|
|
"lease_generation": connected["connection_supervisor"]["lease"]["generation"],
|
|
"endpoint_validation": "host-route",
|
|
"network_reachability": "unreachable",
|
|
"reason_code": "tcp-endpoint-unreachable",
|
|
"host_route_class": "tunnel",
|
|
"write_performed": True,
|
|
"observed_at": connected["connection_verification"]["observed_at"],
|
|
"supervisor_revision": connected["connection_verification"]["supervisor_revision"],
|
|
}
|
|
operation = next(
|
|
item for item in connected["operations"] if item["action"] == "network.provision"
|
|
)
|
|
assert operation["status"] == "succeeded"
|
|
assert operation["result"]["phase"] == "network_applied"
|
|
assert operation["result"]["host_wifi_switch_authorized"] is True
|
|
assert operation["result"]["host_route_ready"] is None
|
|
assert operation["result"]["host_route_class"] is None
|
|
assert connected["connection_attempt"]["phase"] == "network_applied"
|
|
assert connected["connection_attempt"]["control_state"] == "control_not_ready"
|
|
|
|
|
|
def test_bridge_provisioning_separates_address_from_reachable_control_endpoint(
|
|
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,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**__: object,
|
|
) -> dict[str, Any]:
|
|
_dispatch_test_network_write(on_write_dispatch)
|
|
return {
|
|
"started_at_utc": "2026-08-06T10:00:00Z",
|
|
"completed_at_utc": "2026-08-06T10:00:01Z",
|
|
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
|
"outcome": "lan_address_observed",
|
|
"write_mode": "with_response",
|
|
"observations": [{"status": _wifi_status_read("192.168.68.50")["status"]}],
|
|
}
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", fake_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, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
_unreachable_control_endpoint,
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_control_endpoint_reachable",
|
|
lambda _target: False,
|
|
)
|
|
|
|
async def connect_and_join_read_only_continuation() -> dict[str, Any]:
|
|
await service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
connection_mode="bridge",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
with service._lock: # noqa: SLF001
|
|
task = service._control_bootstrap_continuation_task # noqa: SLF001
|
|
if task is not None:
|
|
with pytest.raises(facade_module.ConnectionVerificationError):
|
|
await task
|
|
return service.state()
|
|
|
|
state = asyncio.run(connect_and_join_read_only_continuation())
|
|
|
|
assert state["k1_ip"] == "192.168.68.50"
|
|
assert state["phase"] == "device_selected"
|
|
assert state["device_session"]["connectivity"] == "offline"
|
|
assert state["connection_verification"] == {
|
|
"status": "endpoint-unreachable",
|
|
"lease_state": "configured-unverified",
|
|
"lease_generation": state["connection_supervisor"]["lease"]["generation"],
|
|
"endpoint_validation": "provisioning-status+mqtt-tcp-connect",
|
|
"network_reachability": "unreachable",
|
|
"reason_code": "tcp-endpoint-unreachable",
|
|
"host_route_class": "direct-or-routed",
|
|
"write_performed": True,
|
|
"observed_at": state["connection_verification"]["observed_at"],
|
|
"supervisor_revision": state["connection_verification"]["supervisor_revision"],
|
|
}
|
|
operation = next(item for item in state["operations"] if item["action"] == "network.provision")
|
|
assert operation["status"] == "succeeded"
|
|
assert operation["stage_code"] == "network-configured"
|
|
assert operation["result"]["phase"] == "network_applied"
|
|
assert operation["result"]["control_state"] == "unknown"
|
|
assert operation["result"]["control_endpoint_reachable"] is None
|
|
assert state["connection_attempt"]["phase"] == "network_applied"
|
|
assert state["connection_attempt"]["control_state"] == "control_not_ready"
|
|
|
|
|
|
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)
|
|
mode_revision = _select_connection_mode(service, "quick-connect")
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
_reachable_control_endpoint,
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_control_endpoint_reachable",
|
|
lambda _target: True,
|
|
)
|
|
_set_scanned_devices(service, [{"device_id": "k1-a", "name": "XGR-TEST-A"}])
|
|
activation_calls: list[tuple[str, 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,
|
|
*,
|
|
write_mode: str,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**_: object,
|
|
) -> AsyncIterator[dict[str, Any]]:
|
|
nonlocal ble_session_open
|
|
activation_calls.append((device_id, write_mode))
|
|
_dispatch_test_network_write(on_write_dispatch)
|
|
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",
|
|
"observations": [{"status": _ap_ready_wifi_status()}],
|
|
}
|
|
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": "exact-firmware-profile",
|
|
}
|
|
|
|
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)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
|
|
state = asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
connection_mode="quick-connect",
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
expected_mode_revision=mode_revision,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert activation_calls == [("k1-a", "with_response")]
|
|
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"
|
|
)
|
|
assert state["connection_lifecycle"]["connection_ready"] is True
|
|
|
|
control = state["application_control_session"]
|
|
workspace = service.enter_application_workspace(
|
|
EnterApplicationWorkspaceRequest(
|
|
operator_confirmed=True,
|
|
expected_session_generation=control["session_generation"],
|
|
expected_state_revision=control["state_revision"],
|
|
)
|
|
)
|
|
workspace_control = workspace["application_control_session"]
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
expected_control_session_generation=workspace_control["session_generation"],
|
|
expected_control_state_revision=workspace_control["state_revision"],
|
|
)
|
|
)
|
|
assert prepared["acquisition"]["target_host"] == "192.168.56.1"
|
|
|
|
|
|
def test_quick_connect_read_only_verify_requires_fresh_ble_status_without_write(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_select_connection_mode(service, "quick-connect")
|
|
_seed_supervised_connection(
|
|
service,
|
|
target_ipv4="192.168.56.1",
|
|
connection_mode="quick-connect",
|
|
transport_ref="k1-a",
|
|
logical_device_id="device-k1-a",
|
|
with_control=False,
|
|
)
|
|
service._device_session_id = "session-k1-a" # noqa: SLF001
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
supervisor_generation = service._connection_supervisor.snapshot().lease.generation # noqa: SLF001
|
|
service._connection_lease_generation = supervisor_generation # noqa: SLF001
|
|
service._connection_verification = { # noqa: SLF001
|
|
"status": "endpoint-unreachable",
|
|
"lease_state": "disconnected",
|
|
"lease_generation": supervisor_generation,
|
|
"endpoint_validation": "provisioning-status+mqtt-tcp-connect",
|
|
"network_reachability": "unreachable",
|
|
"reason_code": "connection_lease_endpoint_unreachable_after_provision",
|
|
"write_performed": True,
|
|
"observed_at": "2026-08-06T10:00:00Z",
|
|
}
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_control_endpoint_reachable",
|
|
lambda _target: True,
|
|
)
|
|
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
|
|
monkeypatch.setattr(facade_module, "_host_route_class", lambda _target: "device-ap")
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
|
|
status_reads: list[tuple[str, float, bool]] = []
|
|
|
|
async def exact_ble_status(
|
|
device_id: str,
|
|
*,
|
|
timeout_seconds: float,
|
|
rediscover: bool,
|
|
**_: object,
|
|
) -> dict[str, Any]:
|
|
status_reads.append((device_id, timeout_seconds, rediscover))
|
|
status = _wifi_status_read("192.168.56.1", device_id=device_id)
|
|
status["status"] = _ap_ready_wifi_status()
|
|
return status
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", exact_ble_status)
|
|
|
|
state = asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="k1-a",
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
expected_discovery_generation=0,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert status_reads == [("k1-a", 20.0, False)]
|
|
assert state["connection_verification"]["lease_state"] == "reachable"
|
|
assert state["connection_verification"]["network_reachability"] == "reachable"
|
|
assert state["device_session"]["connectivity"] == "connected"
|
|
assert state["connection_verification"]["write_performed"] is False
|
|
assert state["connection_verification"]["endpoint_validation"] == (
|
|
"ble-wifi-status-read+mqtt-tcp-connect"
|
|
)
|
|
assert state["last_operation"]["action"] == "connection.verify"
|
|
assert state["last_operation"]["status"] == "succeeded"
|
|
|
|
|
|
def test_direct_connect_reuses_the_reviewed_ble_provisioning_frame(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
mode_revision = _select_connection_mode(service, "direct-connect")
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
_reachable_control_endpoint,
|
|
)
|
|
_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,
|
|
*,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**_: object,
|
|
) -> dict[str, Any]:
|
|
provisioning_calls.append((device_id, ssid, password))
|
|
_dispatch_test_network_write(on_write_dispatch)
|
|
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": _wifi_status_read("172.20.10.2")["status"]}],
|
|
}
|
|
|
|
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",
|
|
)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_control_endpoint_reachable",
|
|
lambda _target: True,
|
|
)
|
|
|
|
state = asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="controller-hotspot",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
connection_mode="direct-connect",
|
|
compatibility_attestation=DIRECT_CONNECT_ATTESTATION,
|
|
expected_mode_revision=mode_revision,
|
|
)
|
|
)
|
|
)
|
|
|
|
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)
|
|
mode_revision = _select_connection_mode(service, "quick-connect")
|
|
_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(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
connection_mode="quick-connect",
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
expected_mode_revision=mode_revision,
|
|
)
|
|
)
|
|
)
|
|
|
|
state = service.state()
|
|
assert state["selected_device_id"] is None
|
|
assert state["device_session"] is None
|
|
assert state["k1_ip"] is None
|
|
assert state["connection_mode"] is None
|
|
failed_sessions = list(service.evidence_root.glob("*viewer_k1_ap_association*"))
|
|
assert len(failed_sessions) == 1
|
|
assert (failed_sessions[0] / "operation-reservation.redacted.json").exists()
|
|
assert not (failed_sessions[0] / "ap-activation.redacted.json").exists()
|
|
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)
|
|
mode_revision = _select_connection_mode(service, "quick-connect")
|
|
_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,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**__: object,
|
|
) -> AsyncIterator[dict[str, Any]]:
|
|
assert on_write_dispatch is not None
|
|
on_write_dispatch(
|
|
_wifi_status_read(None, device_id="k1-a")["status"],
|
|
"with_response",
|
|
)
|
|
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",
|
|
"observations": [],
|
|
}
|
|
|
|
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(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
connection_mode="quick-connect",
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
expected_mode_revision=mode_revision,
|
|
)
|
|
)
|
|
)
|
|
|
|
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()
|
|
reconciliation = service.state()["network_write_reconciliation"]
|
|
assert reconciliation["transport_ref"] == "k1-a"
|
|
assert reconciliation["status"] == "previous-session-result-unknown"
|
|
assert reconciliation["reason_code"] == "network-mutation-audit-open"
|
|
assert reconciliation["blocks_new_explicit_intent"] is False
|
|
assert reconciliation["operation_stage"] == "observing"
|
|
assert reconciliation["scope"] == "durable-ledger"
|
|
assert reconciliation["ledger_revision"] == 3
|
|
|
|
|
|
def test_quick_connect_unchanged_ready_baseline_does_not_associate_host_wifi(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
mode_revision = _select_connection_mode(service, "quick-connect")
|
|
_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": True,
|
|
"credential_source": "exact-firmware-profile",
|
|
},
|
|
)
|
|
ready_status = _ap_ready_wifi_status()
|
|
|
|
@asynccontextmanager
|
|
async def unchanged_ready_session(
|
|
*_: object,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**__: object,
|
|
) -> AsyncIterator[dict[str, Any]]:
|
|
assert on_write_dispatch is not None
|
|
on_write_dispatch(ready_status, "with_response")
|
|
yield {
|
|
"profile_id": "xgrids-k1-fw3-quick-connect-ap-v1",
|
|
"started_at_utc": "2026-08-06T10:00:00Z",
|
|
"completed_at_utc": "2026-08-06T10:00:01Z",
|
|
"outcome": "ap_ready_observed",
|
|
"baseline_status": ready_status,
|
|
"ready_observed": True,
|
|
"write_performed": True,
|
|
"write_mode": "with_response",
|
|
"observations": [{"status": ready_status}],
|
|
}
|
|
|
|
def forbidden_association(*_: object, **__: object) -> dict[str, Any]:
|
|
raise AssertionError("baseline/previous AP-ready is not authority to mutate host Wi-Fi")
|
|
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"device_ap_activation_session",
|
|
unchanged_ready_session,
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"associate_with_wifi_profile_once",
|
|
forbidden_association,
|
|
)
|
|
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as raised:
|
|
asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
connection_mode="quick-connect",
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
expected_mode_revision=mode_revision,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert raised.value.reason_code == (
|
|
"network-provision-target-not-distinguishable-from-baseline"
|
|
)
|
|
state = service.state()
|
|
assert state["network_mutation_ledger"]["status"] == "unresolved"
|
|
assert state["network_mutation_ledger"]["stage"] == "observing"
|
|
assert state["semantic_topology_store"]["status"] == "empty"
|
|
assert state["selected_device_id"] is None
|
|
operation = next(item for item in state["operations"] if item["action"] == "network.provision")
|
|
assert operation["error"]["code"] == (
|
|
"network-provision-target-not-distinguishable-from-baseline"
|
|
)
|
|
assert operation["error"]["side_effect_status"] == "unknown"
|
|
assert operation["error"]["safe_to_retry"] is False
|
|
|
|
|
|
def test_failed_connection_change_revokes_the_previous_route(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
mode_revision = _select_connection_mode(service, "quick-connect")
|
|
_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,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**__: object,
|
|
) -> AsyncIterator[dict[str, Any]]:
|
|
assert on_write_dispatch is not None
|
|
on_write_dispatch(
|
|
_wifi_status_read(None, device_id="k1-a")["status"],
|
|
"with_response",
|
|
)
|
|
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",
|
|
"observations": [{"status": _ap_ready_wifi_status()}],
|
|
}
|
|
|
|
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(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
connection_mode="quick-connect",
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
expected_mode_revision=mode_revision,
|
|
)
|
|
)
|
|
)
|
|
|
|
state = service.state()
|
|
# AP-ready is a semantic device-side commit. Host association may fail,
|
|
# but the old Bridge route must not be resurrected and the new Quick
|
|
# topology remains explicit, configured and offline.
|
|
assert state["selected_device_id"] == "k1-a"
|
|
assert state["k1_ip"] == facade_module.AP_FALLBACK_IPV4
|
|
assert state["connection_mode"] == "quick-connect"
|
|
assert state["compatibility"]["attestation"]["topology"] == "device-ap"
|
|
assert state["device_session"]["connectivity"] == "offline"
|
|
assert state["connection_verification"]["lease_state"] == ("configured-unverified")
|
|
operation = next(item for item in state["operations"] if item["action"] == "network.provision")
|
|
assert operation["error"]["scan_attempt_count"] == 4
|
|
assert operation["error"]["scan_elapsed_ms"] == 15014
|
|
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_quick_connect_keychain_failure_after_ap_write_preserves_side_effect_facts(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
mode_revision = _select_connection_mode(service, "quick-connect")
|
|
_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 ready_activation(
|
|
*_: object,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**__: object,
|
|
) -> AsyncIterator[dict[str, Any]]:
|
|
assert on_write_dispatch is not None
|
|
on_write_dispatch(
|
|
_wifi_status_read(None, device_id="k1-a")["status"],
|
|
"with_response",
|
|
)
|
|
yield {
|
|
"profile_id": "xgrids-k1-fw3-quick-connect-ap-v1",
|
|
"started_at_utc": "2026-08-06T10:00:00Z",
|
|
"completed_at_utc": "2026-08-06T10:00:01Z",
|
|
"outcome": "ap_ready_observed",
|
|
"ready_observed": True,
|
|
"write_performed": True,
|
|
"write_mode": "with_response",
|
|
"observations": [{"status": _ap_ready_wifi_status()}],
|
|
}
|
|
|
|
def failed_post_write_profile_read(*_: object, **__: object) -> dict[str, Any]:
|
|
raise facade_module.HostWifiProfileError(
|
|
"keychain-authorization-required",
|
|
)
|
|
|
|
monkeypatch.setattr(facade_module, "device_ap_activation_session", ready_activation)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"associate_with_wifi_profile_once",
|
|
failed_post_write_profile_read,
|
|
)
|
|
|
|
with pytest.raises(
|
|
facade_module.HostWifiProfileError,
|
|
match="keychain-authorization-required",
|
|
):
|
|
asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
connection_mode="quick-connect",
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
expected_mode_revision=mode_revision,
|
|
)
|
|
)
|
|
)
|
|
|
|
state = service.state()
|
|
operation = next(item for item in state["operations"] if item["action"] == "network.provision")
|
|
assert operation["stage_code"] == "host-wifi-association-failed"
|
|
assert operation["error"]["code"] == "keychain-authorization-required"
|
|
assert operation["error"]["host_diagnostic"]["code"] == ("host.keychain.interaction-required")
|
|
assert operation["error"]["side_effect_status"] == "confirmed"
|
|
assert operation["error"]["safe_to_retry"] is False
|
|
assert "helper_stage" not in operation["error"]
|
|
assert state["network_write_reconciliation"] is None
|
|
|
|
|
|
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,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**__: object,
|
|
) -> dict[str, Any]:
|
|
baseline_status = _ap_ready_wifi_status()
|
|
assert on_write_dispatch is not None
|
|
on_write_dispatch(baseline_status, "with_response")
|
|
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",
|
|
"write_mode": "with_response",
|
|
"baseline_status": baseline_status,
|
|
"observations": [
|
|
{
|
|
"status": {
|
|
"mode": "WIFI_CLIENT",
|
|
"ipv4": "10.255.254.51",
|
|
"status_code": 1,
|
|
"reserved": 0,
|
|
}
|
|
}
|
|
],
|
|
}
|
|
|
|
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(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
state = service.state()
|
|
# The K1 already confirmed its new device-side topology before the local
|
|
# address conflict was discovered. Preserve that truth while withholding
|
|
# all host/control authority.
|
|
assert state["selected_device_id"] == "k1-a"
|
|
assert state["k1_ip"] == "10.255.254.51"
|
|
assert state["connection_mode"] == "bridge"
|
|
assert state["device_session"]["connectivity"] == "offline"
|
|
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"] == "confirmed"
|
|
assert state["network_write_reconciliation"] is None
|
|
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 == "confirmed"
|
|
assert failure_log.network_change_attempted is True
|
|
assert failure_log.device_write_attempted is True
|
|
assert failure_log.device_write_confirmed is True
|
|
assert PRIMARY_TEST_CREDENTIAL not in caplog.text
|
|
assert "lab-network" not in caplog.text
|
|
|
|
|
|
def test_ambiguous_ble_write_is_audit_only_for_next_explicit_intent(
|
|
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"}])
|
|
calls = 0
|
|
|
|
async def ambiguous_write(
|
|
*_: object,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**__: object,
|
|
) -> dict[str, Any]:
|
|
nonlocal calls
|
|
calls += 1
|
|
assert on_write_dispatch is not None
|
|
on_write_dispatch(
|
|
_wifi_status_read(None, device_id="k1-a")["status"],
|
|
"with_response",
|
|
)
|
|
exc = RuntimeError("synthetic transport failure")
|
|
exc.operation_stage = "gatt-write" # type: ignore[attr-defined]
|
|
exc.device_write_attempted = True # type: ignore[attr-defined]
|
|
exc.device_write_confirmed = False # type: ignore[attr-defined]
|
|
exc.att_error_code = 4 # type: ignore[attr-defined]
|
|
exc.att_error_name = "INVALID_PDU" # type: ignore[attr-defined]
|
|
exc.resolved_write_mode = "with_response" # type: ignore[attr-defined]
|
|
exc.write_characteristic_properties = ( # type: ignore[attr-defined]
|
|
"notify",
|
|
"write",
|
|
)
|
|
exc.max_write_without_response_size = 253 # type: ignore[attr-defined]
|
|
exc.frame_length = 99 # type: ignore[attr-defined]
|
|
raise exc
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", ambiguous_write)
|
|
|
|
with (
|
|
caplog.at_level(logging.ERROR, logger=facade_module.__name__),
|
|
pytest.raises(RuntimeError, match="synthetic transport failure"),
|
|
):
|
|
asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
idempotency_key="ambiguous-write-1",
|
|
)
|
|
)
|
|
)
|
|
|
|
state = service.state()
|
|
operation = next(item for item in state["operations"] if item["action"] == "network.provision")
|
|
assert operation["error"] == {
|
|
"category": "device",
|
|
"code": "RuntimeError",
|
|
"retryable": False,
|
|
"safe_to_retry": False,
|
|
"side_effect_status": "unknown",
|
|
"operation_stage": "gatt-write",
|
|
"device_write_attempted": True,
|
|
"device_write_confirmed": False,
|
|
"ble_att_error_code": 4,
|
|
"ble_att_error_name": "INVALID_PDU",
|
|
"resolved_write_mode": "with_response",
|
|
"max_write_without_response_size": 253,
|
|
"frame_length": 99,
|
|
"write_characteristic_properties": ["notify", "write"],
|
|
}
|
|
audit = state["network_write_reconciliation"]
|
|
assert audit["status"] == "previous-session-result-unknown"
|
|
assert audit["operation_id"] == operation["operation_id"]
|
|
assert audit["reason_code"] == "network-mutation-audit-open"
|
|
assert audit["required_action"] == "none"
|
|
assert audit["blocks_new_explicit_intent"] is False
|
|
failure_log = next(
|
|
record
|
|
for record in caplog.records
|
|
if getattr(record, "event_code", None) == "k1_network_provision_failed"
|
|
)
|
|
assert failure_log.resolved_write_mode == "with_response"
|
|
assert failure_log.write_characteristic_properties == ["notify", "write"]
|
|
assert failure_log.max_write_without_response_size == 253
|
|
assert failure_log.frame_length == 99
|
|
|
|
restarted_service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_devices(restarted_service, [{"device_id": "k1-a"}])
|
|
restarted_state = restarted_service.state()
|
|
assert restarted_state["network_write_reconciliation"] is None
|
|
assert restarted_state["network_mutation_ledger"]["mutation_allowed"] is True
|
|
assert restarted_state["network_mutation_ledger"]["resolution"] == "interrupted"
|
|
assert (
|
|
restarted_state["connection_policy"]["actions"]["provision-fresh-device"]["allowed"] is True
|
|
)
|
|
assert calls == 1
|
|
|
|
|
|
def test_unchanged_status_does_not_confirm_without_response_write(
|
|
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"}])
|
|
unchanged_status = _wifi_status_read(None, device_id="k1-a")["status"]
|
|
|
|
async def unchanged_write(
|
|
*_: object,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**__: object,
|
|
) -> dict[str, Any]:
|
|
assert on_write_dispatch is not None
|
|
on_write_dispatch(unchanged_status, "without_response")
|
|
return {
|
|
"started_at_utc": "2026-08-06T10:00:00Z",
|
|
"completed_at_utc": "2026-08-06T10:00:01Z",
|
|
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
|
"outcome": "no_status_change_before_timeout",
|
|
"write_mode": "without_response",
|
|
"baseline_status": unchanged_status,
|
|
"observations": [{"status": unchanged_status}],
|
|
}
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", unchanged_write)
|
|
|
|
with (
|
|
caplog.at_level(logging.ERROR, logger=facade_module.__name__),
|
|
pytest.raises(RuntimeError, match="не сообщило адрес"),
|
|
):
|
|
asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
record = next(
|
|
item
|
|
for item in caplog.records
|
|
if getattr(item, "event_code", None) == "k1_network_provision_failed"
|
|
)
|
|
assert record.device_write_attempted is True
|
|
assert record.device_write_confirmed is False
|
|
assert record.side_effect_status == "unknown"
|
|
fence = service.state()["network_write_reconciliation"]
|
|
assert fence is not None
|
|
assert fence["transport_ref"] == "k1-a"
|
|
assert fence["connection_mode"] == "bridge"
|
|
assert fence["operation_stage"] == "observing"
|
|
assert fence["reason_code"] == "network-mutation-audit-open"
|
|
assert fence["required_action"] == "none"
|
|
assert fence["blocks_new_explicit_intent"] is False
|
|
assert fence["scope"] == "durable-ledger"
|
|
assert fence["ledger_revision"] == 3
|
|
ledger = service.state()["network_mutation_ledger"]
|
|
assert ledger["status"] == "unresolved"
|
|
assert ledger["mutation_allowed"] is True
|
|
assert ledger["stage"] == "observing"
|
|
assert ledger["revision"] == 3
|
|
|
|
|
|
def test_same_bridge_baseline_does_not_confirm_with_response_write(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_devices(service, [{"device_id": "k1-a"}])
|
|
unchanged_status = _wifi_status_read("192.168.68.50", device_id="k1-a")["status"]
|
|
|
|
async def unchanged_write(
|
|
*_: object,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**__: object,
|
|
) -> dict[str, Any]:
|
|
assert on_write_dispatch is not None
|
|
on_write_dispatch(unchanged_status, "with_response")
|
|
return {
|
|
"started_at_utc": "2026-08-06T10:00:00Z",
|
|
"completed_at_utc": "2026-08-06T10:00:01Z",
|
|
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
|
"outcome": "no_status_change_before_timeout",
|
|
"write_mode": "with_response",
|
|
"baseline_status": unchanged_status,
|
|
"observations": [{"status": unchanged_status}],
|
|
}
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", unchanged_write)
|
|
|
|
with pytest.raises(RuntimeError, match="не сообщило адрес"):
|
|
asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="replacement-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
state = service.state()
|
|
assert state["network_mutation_ledger"]["status"] == "unresolved"
|
|
assert state["network_mutation_ledger"]["stage"] == "observing"
|
|
assert state["network_mutation_ledger"]["resolution"] is None
|
|
assert state["network_write_reconciliation"] is not None
|
|
assert state["semantic_topology_store"]["status"] == "empty"
|
|
assert state["selected_device_id"] is None
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("connection_mode", "attestation", "observed_ipv4"),
|
|
[
|
|
("bridge", ATTESTATION, "192.168.68.99"),
|
|
("direct-connect", DIRECT_CONNECT_ATTESTATION, "172.20.10.2"),
|
|
],
|
|
)
|
|
def test_live_client_baseline_with_different_ipv4_keeps_write_ambiguous(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
connection_mode: NetworkConnectionMode,
|
|
attestation: CompatibilityAttestationRequest,
|
|
observed_ipv4: str,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
mode_revision = _select_connection_mode(service, connection_mode)
|
|
_set_scanned_devices(service, [{"device_id": "k1-a"}])
|
|
baseline = _wifi_status_read("192.168.68.40", device_id="k1-a")["status"]
|
|
|
|
async def changed_client_address(
|
|
*_: object,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**__: object,
|
|
) -> dict[str, Any]:
|
|
assert on_write_dispatch is not None
|
|
on_write_dispatch(baseline, "with_response")
|
|
return {
|
|
"started_at_utc": "2026-08-06T10:00:00Z",
|
|
"completed_at_utc": "2026-08-06T10:00:01Z",
|
|
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
|
"outcome": "lan_address_observed",
|
|
"write_mode": "with_response",
|
|
"baseline_status": baseline,
|
|
"observations": [
|
|
{
|
|
"status": _wifi_status_read(
|
|
observed_ipv4,
|
|
device_id="k1-a",
|
|
)["status"]
|
|
}
|
|
],
|
|
}
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", changed_client_address)
|
|
|
|
with pytest.raises(RuntimeError, match="не сообщило адрес"):
|
|
asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="replacement-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
connection_mode=connection_mode,
|
|
compatibility_attestation=attestation,
|
|
expected_mode_revision=mode_revision,
|
|
)
|
|
)
|
|
)
|
|
|
|
state = service.state()
|
|
assert state["network_mutation_ledger"]["status"] == "unresolved"
|
|
assert state["network_mutation_ledger"]["stage"] == "observing"
|
|
assert state["network_mutation_ledger"]["resolution"] is None
|
|
assert state["network_write_reconciliation"] is not None
|
|
assert state["semantic_topology_store"]["status"] == "empty"
|
|
assert state["selected_device_id"] is None
|
|
|
|
|
|
def test_read_only_reconciliation_terminalizes_the_matching_idempotency_edge(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service)
|
|
_seed_unresolved_network_mutation(
|
|
service,
|
|
operation_id="lost-response-operation",
|
|
)
|
|
reads = 0
|
|
|
|
async def read_current_status(*_: object, **__: object) -> dict[str, Any]:
|
|
nonlocal reads
|
|
reads += 1
|
|
return _wifi_status_read("192.168.68.50")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", read_current_status)
|
|
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, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", lambda _target: True)
|
|
|
|
state = asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="test-ble-transport",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_discovery_generation=0,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert reads == 1
|
|
assert state["connection_mode"] == "bridge"
|
|
assert state["k1_ip"] == "192.168.68.50"
|
|
assert state["network_write_reconciliation"] is None
|
|
assert state["network_mutation_ledger"]["status"] == "resolved"
|
|
assert state["network_mutation_ledger"]["resolution"] == "target-observed"
|
|
assert state["network_provisioning_idempotency"] == {
|
|
"status": "ready",
|
|
"revision": state["network_provisioning_idempotency"]["revision"],
|
|
"reason_code": None,
|
|
"active_operation_id": None,
|
|
"active_action": None,
|
|
"active_stage": None,
|
|
"terminal_record_count": 1,
|
|
"mutation_allowed": True,
|
|
}
|
|
idempotency_record = (
|
|
service._require_network_provisioning_idempotency_journal() # noqa: SLF001
|
|
.snapshot()
|
|
.records[0]
|
|
)
|
|
assert idempotency_record.operation_id == "lost-response-operation"
|
|
assert idempotency_record.stage == "terminal"
|
|
assert idempotency_record.terminal is not None
|
|
assert idempotency_record.terminal.outcome == "succeeded"
|
|
assert idempotency_record.terminal.outcome_code == "network.provision.reconciled"
|
|
assert idempotency_record.terminal.side_effect_status == "reconciled"
|
|
|
|
|
|
def test_restart_interrupts_legacy_edge_before_optional_read_only_verify(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
operation_id = "legacy-read-only-reconciliation"
|
|
_seed_legacy_network_mutation_without_idempotency(
|
|
service,
|
|
operation_id=operation_id,
|
|
stage="observing",
|
|
)
|
|
|
|
restarted, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(restarted)
|
|
reads = 0
|
|
|
|
async def read_current_status(*_: object, **__: object) -> dict[str, Any]:
|
|
nonlocal reads
|
|
reads += 1
|
|
return _wifi_status_read("192.168.68.50")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", read_current_status)
|
|
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, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", lambda _target: True)
|
|
|
|
state = asyncio.run(
|
|
restarted.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="test-ble-transport",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_discovery_generation=0,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert reads == 1
|
|
assert state["network_write_reconciliation"] is None
|
|
assert state["network_mutation_ledger"]["status"] == "resolved"
|
|
assert state["network_mutation_ledger"]["resolution"] == "interrupted"
|
|
idempotency = restarted._require_network_provisioning_idempotency_journal().snapshot() # noqa: SLF001
|
|
assert idempotency.status == "ready"
|
|
assert idempotency.active_record is None
|
|
terminal = idempotency.records[-1]
|
|
assert terminal.operation_id == operation_id
|
|
assert terminal.stage == "terminal"
|
|
assert terminal.terminal is not None
|
|
assert terminal.terminal.outcome_code == "network.provision.interrupted"
|
|
assert terminal.terminal.side_effect_status == "reconciled"
|
|
|
|
|
|
def test_read_only_reconciliation_rejects_cross_journal_mismatch_before_gatt(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service)
|
|
fence = _seed_unresolved_network_mutation(
|
|
service,
|
|
operation_id="network-ledger-operation",
|
|
idempotency_operation_id="idempotency-journal-operation",
|
|
)
|
|
|
|
async def forbidden_status_read(*_: object, **__: object) -> dict[str, Any]:
|
|
raise AssertionError("cross-journal mismatch must fail before GATT")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_status_read)
|
|
|
|
with pytest.raises(facade_module.ConnectionVerificationError) as raised:
|
|
asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="test-ble-transport",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_discovery_generation=0,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert raised.value.reason_code == "connection-verify-idempotency-operation-mismatch"
|
|
state = service.state()
|
|
assert state["network_write_reconciliation"] == fence
|
|
assert state["network_mutation_ledger"]["status"] == "unresolved"
|
|
assert state["network_provisioning_idempotency"]["status"] == "blocked"
|
|
assert state["network_provisioning_idempotency"]["active_operation_id"] == (
|
|
"idempotency-journal-operation"
|
|
)
|
|
|
|
|
|
def test_read_only_addressless_status_keeps_network_write_reconciliation_fence(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service)
|
|
_seed_unresolved_network_mutation(service)
|
|
|
|
async def read_current_status(*_: object, **__: object) -> dict[str, Any]:
|
|
return _wifi_status_read(None)
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", read_current_status)
|
|
|
|
with pytest.raises(
|
|
facade_module.ConnectionVerificationError,
|
|
match="не сообщил адрес общей локальной сети",
|
|
) as raised:
|
|
asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="test-ble-transport",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_discovery_generation=0,
|
|
)
|
|
)
|
|
)
|
|
assert raised.value.reason_code == "connection-verify-address-unavailable"
|
|
|
|
fence = service.state()["network_write_reconciliation"]
|
|
assert fence is not None
|
|
assert fence["operation_id"] == "ambiguous-operation"
|
|
assert fence["operation_stage"] == "dispatching"
|
|
assert fence["scope"] == "durable-ledger"
|
|
assert fence["ledger_revision"] == 2
|
|
|
|
|
|
def test_read_only_ble_status_for_another_transport_keeps_reconciliation_fence(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-b")
|
|
fence = _seed_unresolved_network_mutation(service, transport_ref="k1-a")
|
|
|
|
async def read_other_status(*_: object, **__: object) -> dict[str, Any]:
|
|
raise AssertionError("target mismatch must fail before a GATT read")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", read_other_status)
|
|
|
|
with pytest.raises(
|
|
facade_module.ConnectionVerificationError,
|
|
match="относится к другому K1",
|
|
) as raised:
|
|
asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="k1-b",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_discovery_generation=0,
|
|
)
|
|
)
|
|
)
|
|
assert raised.value.reason_code == "connection-verify-reconciliation-target-mismatch"
|
|
|
|
assert service.state()["network_write_reconciliation"] == fence
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("intended_mode", "attestation", "requested_mode"),
|
|
[
|
|
("bridge", DIRECT_CONNECT_ATTESTATION, "direct-connect"),
|
|
("direct-connect", ATTESTATION, "bridge"),
|
|
],
|
|
)
|
|
def test_read_only_reconciliation_requires_exact_requested_mode_despite_shared_status(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
intended_mode: NetworkConnectionMode,
|
|
attestation: CompatibilityAttestationRequest,
|
|
requested_mode: str,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service)
|
|
original_fence = _seed_unresolved_network_mutation(
|
|
service,
|
|
intended_mode=intended_mode,
|
|
)
|
|
|
|
async def read_current_status(*_: object, **__: object) -> dict[str, Any]:
|
|
raise AssertionError("mode mismatch must fail before a GATT read")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", read_current_status)
|
|
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)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
|
|
with pytest.raises(facade_module.ConnectionVerificationError) as raised:
|
|
asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="test-ble-transport",
|
|
compatibility_attestation=attestation,
|
|
expected_discovery_generation=0,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert raised.value.reason_code == "connection-verify-reconciliation-target-mismatch"
|
|
state = service.state()
|
|
assert state["connection_mode"] is None
|
|
assert state["k1_ip"] is None
|
|
assert state["network_write_reconciliation"] == original_fence
|
|
ledger = state["network_mutation_ledger"]
|
|
assert ledger["status"] == "unresolved"
|
|
assert ledger["mutation_allowed"] is True
|
|
assert ledger["intended_mode"] == intended_mode
|
|
assert ledger["revision"] == 2
|
|
assert ledger["resolution"] is None
|
|
verify_operation = next(
|
|
item for item in state["operations"] if item["action"] == "connection.verify"
|
|
)
|
|
assert verify_operation["status"] == "failed"
|
|
|
|
|
|
def test_durable_restart_interrupts_old_attempt_without_implicit_gatt_recovery(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
first, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_unresolved_network_mutation(
|
|
first,
|
|
operation_id="durable-restart-reconciliation",
|
|
transport_ref=DURABLE_K1_UUID,
|
|
)
|
|
|
|
restarted, _ = service_with_fake_runtime(tmp_path)
|
|
captured = _durable_status_capture()
|
|
status_reads: list[tuple[str, bool, str | None]] = []
|
|
pin_calls: list[tuple[object, str]] = []
|
|
write_calls: list[str] = []
|
|
|
|
async def read_server_target(
|
|
device_id: str,
|
|
*,
|
|
captured_device: object | None = None,
|
|
recovery_device_session_id: str | None = None,
|
|
allow_known_device_retrieval: bool = False,
|
|
on_gatt_validated: Callable[[object], None] | None = None,
|
|
**_: object,
|
|
) -> dict[str, Any]:
|
|
status_reads.append((device_id, allow_known_device_retrieval, recovery_device_session_id))
|
|
assert captured_device is None
|
|
assert on_gatt_validated is not None
|
|
on_gatt_validated(captured)
|
|
return _wifi_status_read("192.168.68.50", device_id=device_id)
|
|
|
|
async def forbidden_write(*_: object, **__: object) -> dict[str, Any]:
|
|
write_calls.append("write")
|
|
raise AssertionError("durable reconciliation must not write to K1")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", read_server_target)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_write)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"pin_connected_device_handle",
|
|
lambda handle, *, device_session_id: pin_calls.append((handle, device_session_id)),
|
|
)
|
|
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, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", lambda _target: True)
|
|
|
|
before = restarted.state()
|
|
assert before["devices"] == []
|
|
assert before["selected_device_id"] is None
|
|
assert before["network_mutation_ledger"]["status"] == "resolved"
|
|
assert before["network_mutation_ledger"]["resolution"] == "interrupted"
|
|
|
|
with pytest.raises(facade_module.ConnectionVerificationError) as raised:
|
|
asyncio.run(
|
|
restarted.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id=DURABLE_K1_UUID,
|
|
source="durable-configured-state",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
assert raised.value.reason_code == "connection-verify-candidate-not-fresh"
|
|
state = restarted.state()
|
|
assert status_reads == []
|
|
assert write_calls == []
|
|
assert pin_calls == []
|
|
assert state["selected_device_id"] is None
|
|
assert state["device_session"] is None
|
|
assert state["network_write_reconciliation"] is None
|
|
assert state["network_mutation_ledger"]["status"] == "resolved"
|
|
assert state["network_mutation_ledger"]["resolution"] == "interrupted"
|
|
assert state["network_provisioning_idempotency"]["status"] == "ready"
|
|
assert state["network_provisioning_idempotency"]["active_operation_id"] is None
|
|
assert state["network_provisioning_idempotency"]["terminal_record_count"] == 1
|
|
terminal = (
|
|
restarted._require_network_provisioning_idempotency_journal() # noqa: SLF001
|
|
.snapshot()
|
|
.records[-1]
|
|
)
|
|
assert terminal.operation_id == "durable-restart-reconciliation"
|
|
assert terminal.stage == "terminal"
|
|
assert terminal.terminal is not None
|
|
assert terminal.terminal.outcome_code == "network.provision.interrupted"
|
|
assert terminal.terminal.side_effect_status == "reconciled"
|
|
assert state["connection_supervisor"]["authority"]["control_allowed"] is False
|
|
|
|
# Without an independently durable current/configured topology, startup's
|
|
# interrupted audit must not turn an arbitrary new advertisement into an
|
|
# implicit recovery continuation.
|
|
_set_scanned_k1(restarted, device_id="replacement-k1")
|
|
fresh_policy = restarted.state()["connection_policy"]
|
|
assert fresh_policy["actions"]["observe-fresh-device-network"]["allowed"] is True
|
|
assert fresh_policy["recommended_action"] == "select-connection-intent"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("device_id", "attestation", "source", "reason_code"),
|
|
[
|
|
(
|
|
"AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE",
|
|
ATTESTATION,
|
|
"durable-configured-state",
|
|
"connection-verify-reconciliation-target-mismatch",
|
|
),
|
|
(
|
|
DURABLE_K1_UUID,
|
|
QUICK_CONNECT_ATTESTATION,
|
|
"durable-configured-state",
|
|
"connection-verify-reconciliation-target-mismatch",
|
|
),
|
|
(
|
|
DURABLE_K1_UUID,
|
|
ATTESTATION,
|
|
"fresh-scan",
|
|
"connection-verify-source-mismatch",
|
|
),
|
|
],
|
|
)
|
|
def test_durable_restart_rejects_request_target_mismatch_before_gatt(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
device_id: str,
|
|
attestation: CompatibilityAttestationRequest,
|
|
source: str,
|
|
reason_code: str,
|
|
) -> None:
|
|
first, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_unresolved_network_mutation(
|
|
first,
|
|
operation_id="durable-request-mismatch",
|
|
transport_ref=DURABLE_K1_UUID,
|
|
)
|
|
restarted, _ = service_with_fake_runtime(tmp_path)
|
|
reads: list[str] = []
|
|
|
|
async def forbidden_read(*_: object, **__: object) -> dict[str, Any]:
|
|
reads.append("read")
|
|
raise AssertionError("request mismatch must fail before GATT")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_read)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"provision_wifi_once",
|
|
lambda *_args, **_kwargs: (_ for _ in ()).throw(
|
|
AssertionError("request mismatch must not write")
|
|
),
|
|
)
|
|
|
|
with pytest.raises(facade_module.ConnectionVerificationError) as raised:
|
|
asyncio.run(
|
|
restarted.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id=device_id,
|
|
source=source, # type: ignore[arg-type]
|
|
compatibility_attestation=attestation,
|
|
**({"expected_discovery_generation": 0} if source == "fresh-scan" else {}),
|
|
)
|
|
)
|
|
)
|
|
|
|
assert raised.value.reason_code == "connection-verify-candidate-not-fresh"
|
|
assert reads == []
|
|
state = restarted.state()
|
|
assert state["network_write_reconciliation"] is None
|
|
assert state["network_mutation_ledger"]["status"] == "resolved"
|
|
assert state["network_mutation_ledger"]["resolution"] == "interrupted"
|
|
assert state["selected_device_id"] is None
|
|
assert state["device_session"] is None
|
|
|
|
|
|
def test_durable_restart_same_bridge_baseline_cannot_resolve_ambiguous_write(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
baseline = NetworkStatusEvidence(
|
|
mode="WIFI_CLIENT",
|
|
ipv4="192.168.68.50",
|
|
status_code=1,
|
|
reserved=0,
|
|
)
|
|
first, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_unresolved_network_mutation(
|
|
first,
|
|
operation_id="durable-unchanged-bridge-baseline",
|
|
transport_ref=DURABLE_K1_UUID,
|
|
baseline_status=baseline,
|
|
)
|
|
restarted, _ = service_with_fake_runtime(tmp_path)
|
|
captured = _durable_status_capture()
|
|
reads = 0
|
|
pin_calls: list[tuple[object, str]] = []
|
|
|
|
async def read_unchanged_bridge_status(
|
|
device_id: str,
|
|
*,
|
|
allow_known_device_retrieval: bool = False,
|
|
on_gatt_validated: Callable[[object], None] | None = None,
|
|
**_: object,
|
|
) -> dict[str, Any]:
|
|
nonlocal reads
|
|
reads += 1
|
|
assert device_id == DURABLE_K1_UUID
|
|
assert allow_known_device_retrieval is True
|
|
assert on_gatt_validated is not None
|
|
on_gatt_validated(captured)
|
|
return _wifi_status_read("192.168.68.50", device_id=device_id)
|
|
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"read_wifi_status_once",
|
|
read_unchanged_bridge_status,
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"pin_connected_device_handle",
|
|
lambda handle, *, device_session_id: pin_calls.append((handle, device_session_id)),
|
|
)
|
|
|
|
with pytest.raises(facade_module.ConnectionVerificationError) as raised:
|
|
asyncio.run(
|
|
restarted.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id=DURABLE_K1_UUID,
|
|
source="durable-configured-state",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert raised.value.reason_code == "connection-verify-candidate-not-fresh"
|
|
assert reads == 0
|
|
assert pin_calls == []
|
|
state = restarted.state()
|
|
assert state["network_write_reconciliation"] is None
|
|
assert state["network_mutation_ledger"]["status"] == "resolved"
|
|
assert state["network_mutation_ledger"]["resolution"] == "interrupted"
|
|
assert state["network_provisioning_idempotency"]["status"] == "ready"
|
|
assert state["semantic_topology_store"]["status"] == "empty"
|
|
assert state["selected_device_id"] is None
|
|
assert state["device_session"] is None
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("intended_mode", "attestation", "baseline", "previous", "observed_status"),
|
|
[
|
|
(
|
|
"quick-connect",
|
|
QUICK_CONNECT_ATTESTATION,
|
|
NetworkStatusEvidence("WIFI_CLIENT", "192.168.68.50", 1, 0),
|
|
PreviousConnectionEvidence(
|
|
DURABLE_K1_UUID,
|
|
"quick-connect",
|
|
"192.168.56.1",
|
|
"previous-quick-session",
|
|
),
|
|
_ap_ready_wifi_status(),
|
|
),
|
|
(
|
|
"bridge",
|
|
ATTESTATION,
|
|
NetworkStatusEvidence("WIFI_AP", "192.168.56.1", 1, 1),
|
|
PreviousConnectionEvidence(
|
|
DURABLE_K1_UUID,
|
|
"bridge",
|
|
"192.168.68.40",
|
|
"previous-bridge-session",
|
|
),
|
|
_wifi_status_read("192.168.68.99")["status"],
|
|
),
|
|
(
|
|
"direct-connect",
|
|
DIRECT_CONNECT_ATTESTATION,
|
|
NetworkStatusEvidence("WIFI_AP", "192.168.56.1", 1, 1),
|
|
PreviousConnectionEvidence(
|
|
DURABLE_K1_UUID,
|
|
"direct-connect",
|
|
"172.20.10.3",
|
|
"previous-direct-session",
|
|
),
|
|
_wifi_status_read("172.20.10.2")["status"],
|
|
),
|
|
],
|
|
)
|
|
def test_durable_restart_previous_topology_cannot_resolve_ambiguous_write(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
intended_mode: NetworkConnectionMode,
|
|
attestation: CompatibilityAttestationRequest,
|
|
baseline: NetworkStatusEvidence,
|
|
previous: PreviousConnectionEvidence,
|
|
observed_status: dict[str, Any],
|
|
) -> None:
|
|
first, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_unresolved_network_mutation(
|
|
first,
|
|
operation_id=f"durable-previous-{intended_mode}",
|
|
transport_ref=DURABLE_K1_UUID,
|
|
intended_mode=intended_mode,
|
|
baseline_status=baseline,
|
|
previous_connection=previous,
|
|
)
|
|
restarted, _ = service_with_fake_runtime(tmp_path)
|
|
captured = _durable_status_capture()
|
|
reads = 0
|
|
pin_calls: list[tuple[object, str]] = []
|
|
write_calls: list[str] = []
|
|
|
|
async def read_previous_topology(
|
|
device_id: str,
|
|
*,
|
|
allow_known_device_retrieval: bool = False,
|
|
on_gatt_validated: Callable[[object], None] | None = None,
|
|
**_: object,
|
|
) -> dict[str, Any]:
|
|
nonlocal reads
|
|
reads += 1
|
|
assert device_id == DURABLE_K1_UUID
|
|
assert allow_known_device_retrieval is True
|
|
assert on_gatt_validated is not None
|
|
on_gatt_validated(captured)
|
|
status_read = _wifi_status_read(None, device_id=device_id)
|
|
status_read["status"] = observed_status
|
|
return status_read
|
|
|
|
async def forbidden_write(*_: object, **__: object) -> dict[str, Any]:
|
|
write_calls.append("write")
|
|
raise AssertionError("read-only restart reconciliation must not write")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", read_previous_topology)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_write)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"pin_connected_device_handle",
|
|
lambda handle, *, device_session_id: pin_calls.append((handle, device_session_id)),
|
|
)
|
|
|
|
with pytest.raises(facade_module.ConnectionVerificationError) as raised:
|
|
asyncio.run(
|
|
restarted.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id=DURABLE_K1_UUID,
|
|
source="durable-configured-state",
|
|
compatibility_attestation=attestation,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert raised.value.reason_code == "connection-verify-candidate-not-fresh"
|
|
assert reads == 0
|
|
assert write_calls == []
|
|
assert pin_calls == []
|
|
state = restarted.state()
|
|
assert state["network_write_reconciliation"] is None
|
|
assert state["network_mutation_ledger"]["status"] == "resolved"
|
|
assert state["network_mutation_ledger"]["resolution"] == "interrupted"
|
|
assert state["network_provisioning_idempotency"]["status"] == "ready"
|
|
assert state["semantic_topology_store"]["status"] == "empty"
|
|
assert state["selected_device_id"] is None
|
|
assert state["device_session"] is None
|
|
|
|
|
|
@pytest.mark.parametrize("outcome", ["baseline", "wrong-mode", "read-failure"])
|
|
def test_durable_restart_failed_status_keeps_ambiguity_and_does_not_pin(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
outcome: str,
|
|
) -> None:
|
|
first, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_unresolved_network_mutation(
|
|
first,
|
|
operation_id=f"durable-{outcome}",
|
|
transport_ref=DURABLE_K1_UUID,
|
|
)
|
|
restarted, _ = service_with_fake_runtime(tmp_path)
|
|
captured = _durable_status_capture()
|
|
reads = 0
|
|
pin_calls: list[tuple[object, str]] = []
|
|
write_calls: list[str] = []
|
|
|
|
async def read_nonmatching_status(
|
|
device_id: str,
|
|
*,
|
|
allow_known_device_retrieval: bool = False,
|
|
on_gatt_validated: Callable[[object], None] | None = None,
|
|
**_: object,
|
|
) -> dict[str, Any]:
|
|
nonlocal reads
|
|
reads += 1
|
|
assert device_id == DURABLE_K1_UUID
|
|
assert allow_known_device_retrieval is True
|
|
if outcome == "read-failure":
|
|
raise RuntimeError("synthetic durable GATT read failure")
|
|
assert on_gatt_validated is not None
|
|
on_gatt_validated(captured)
|
|
result = _wifi_status_read(None, device_id=device_id)
|
|
if outcome == "wrong-mode":
|
|
result["status"] = _ap_ready_wifi_status()
|
|
return result
|
|
|
|
async def forbidden_write(*_: object, **__: object) -> dict[str, Any]:
|
|
write_calls.append("write")
|
|
raise AssertionError("failed status observation must not write")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", read_nonmatching_status)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_write)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"pin_connected_device_handle",
|
|
lambda handle, *, device_session_id: pin_calls.append((handle, device_session_id)),
|
|
)
|
|
|
|
with pytest.raises(facade_module.ConnectionVerificationError):
|
|
asyncio.run(
|
|
restarted.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id=DURABLE_K1_UUID,
|
|
source="durable-configured-state",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert reads == 0
|
|
assert write_calls == []
|
|
assert pin_calls == []
|
|
state = restarted.state()
|
|
assert state["network_mutation_ledger"]["status"] == "resolved"
|
|
assert state["network_mutation_ledger"]["resolution"] == "interrupted"
|
|
assert state["network_provisioning_idempotency"]["status"] == "ready"
|
|
assert state["semantic_topology_store"]["status"] == "empty"
|
|
assert state["selected_device_id"] is None
|
|
assert state["device_session"] is None
|
|
|
|
|
|
def test_semantic_only_durable_restart_uses_lan_and_mqtt_identity_not_ble(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
first, _ = service_with_fake_runtime(tmp_path)
|
|
topology_store = first._semantic_topology_store # noqa: SLF001
|
|
assert topology_store is not None
|
|
topology_store.commit(
|
|
transport_ref=DURABLE_K1_UUID,
|
|
connection_mode="bridge",
|
|
ipv4="192.168.68.50",
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
firmware_version="3.0.2",
|
|
source="ble-read-only-status",
|
|
observed_at_utc="2026-08-06T12:00:00Z",
|
|
)
|
|
_seed_unresolved_network_mutation(
|
|
first,
|
|
operation_id="semantic-restart-post-dispatch-unknown",
|
|
transport_ref=DURABLE_K1_UUID,
|
|
intended_mode="bridge",
|
|
)
|
|
|
|
restarted, _ = service_with_fake_runtime(tmp_path)
|
|
association_probe = FakeHostWifiAssociationProbe("a" * 64)
|
|
restarted._host_wifi_association_probe = association_probe # type: ignore[assignment] # noqa: SLF001
|
|
reads = 0
|
|
pin_calls: list[tuple[object, str]] = []
|
|
binding_validation_epochs: list[tuple[int, int]] = []
|
|
|
|
async def forbidden_status_read(*_: object, **__: object) -> dict[str, Any]:
|
|
nonlocal reads
|
|
reads += 1
|
|
raise AssertionError("resolved durable restart must not require BLE")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_status_read)
|
|
network_writes: list[str] = []
|
|
|
|
async def forbidden_network_write(*_: object, **__: object) -> dict[str, Any]:
|
|
network_writes.append("network-write")
|
|
raise AssertionError("semantic observation must not write")
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_network_write)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"pin_connected_device_handle",
|
|
lambda handle, *, device_session_id: pin_calls.append((handle, device_session_id)),
|
|
)
|
|
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_probe_control_endpoint_socket",
|
|
lambda _target: facade_module.TcpReachabilityProbeResult(reachable=True),
|
|
)
|
|
|
|
async def bootstrap_after_exact_path_validation(
|
|
bound_service: XgridsK1CompatibilityService,
|
|
*,
|
|
parent_operation_id: str | None,
|
|
connection_mode: facade_module.ConnectionMode,
|
|
inspection_only: bool = False,
|
|
) -> None:
|
|
supervisor = bound_service._connection_supervisor.snapshot() # noqa: SLF001
|
|
assert supervisor.intent is not None
|
|
assert supervisor.device_network.target is not None
|
|
assert supervisor.device_network.transport_ref is not None
|
|
binding = ApplicationConnectionBinding(
|
|
intent_id=supervisor.intent.intent_id,
|
|
transport_ref=supervisor.device_network.transport_ref,
|
|
host_path_epoch=supervisor.host_path.epoch,
|
|
target_ipv4=supervisor.device_network.target.ipv4,
|
|
target_port=supervisor.device_network.target.port,
|
|
connection_mode=connection_mode,
|
|
)
|
|
epoch_before = supervisor.host_path.epoch
|
|
bound_service._validate_application_connection_path(binding) # noqa: SLF001
|
|
epoch_after = bound_service._connection_supervisor.snapshot().host_path.epoch # noqa: SLF001
|
|
binding_validation_epochs.append((epoch_before, epoch_after))
|
|
await _synthetic_prestart_control_bootstrap(
|
|
bound_service,
|
|
parent_operation_id=parent_operation_id,
|
|
connection_mode=connection_mode,
|
|
inspection_only=inspection_only,
|
|
)
|
|
|
|
restarted._bootstrap_prestart_control_ready_owned = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
bootstrap_after_exact_path_validation,
|
|
restarted,
|
|
)
|
|
|
|
before = restarted.state()
|
|
assert before["devices"] == []
|
|
assert before["selected_device_id"] is None
|
|
assert before["connection_attempt"] is None
|
|
assert before["network_mutation_ledger"]["status"] == "resolved"
|
|
assert before["network_mutation_ledger"]["resolution"] == "interrupted"
|
|
assert before["semantic_topology_store"]["record"]["revision"] == 1
|
|
assert (
|
|
before["connection_policy"]["actions"]["observe-configured-device-network"][
|
|
"requires_live_gatt_validation"
|
|
]
|
|
is False
|
|
)
|
|
assert (
|
|
before["connection_policy"]["actions"]["observe-configured-device-network"]["allowed"]
|
|
is True
|
|
)
|
|
assert before["connection_policy"]["recommended_action"] == (
|
|
"observe-configured-device-network"
|
|
)
|
|
assert before["connection_policy"]["actions"]["scan-ble"]["allowed"] is True
|
|
|
|
state = asyncio.run(
|
|
restarted.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id=DURABLE_K1_UUID,
|
|
source="durable-configured-state",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert reads == 0
|
|
assert network_writes == []
|
|
assert pin_calls == []
|
|
assert state["semantic_topology_store"]["record"]["revision"] == 1
|
|
assert state["connection_verification"]["endpoint_validation"] == (
|
|
"durable-semantic-topology+mqtt-tcp-connect"
|
|
)
|
|
assert state["connection_verification"]["lease_state"] == "reachable"
|
|
assert state["connection_supervisor"]["observed"]["device_network"]["source"] == (
|
|
"durable-semantic-topology"
|
|
)
|
|
assert state["connection_supervisor"]["observed"]["device_identity"]["state"] == "verified"
|
|
assert state["connection_supervisor"]["authority"]["control_allowed"] is True
|
|
assert state["connection_supervisor"]["authority"]["acquisition_start_allowed"] is True
|
|
assert binding_validation_epochs == [(1, 1)]
|
|
assert association_probe.interfaces == ["test0", "test0", "test0"]
|
|
assert state["connection_supervisor"]["observed"]["host_path"]["fingerprint"] != (
|
|
"test-route:192.168.68.50"
|
|
)
|
|
|
|
|
|
def test_semantic_durable_verify_refreshes_stale_dhcp_over_exact_read_only_ble(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
first, _ = service_with_fake_runtime(tmp_path)
|
|
topology_store = first._semantic_topology_store # noqa: SLF001
|
|
assert topology_store is not None
|
|
topology_store.commit(
|
|
transport_ref=DURABLE_K1_UUID,
|
|
connection_mode="bridge",
|
|
ipv4="192.168.68.50",
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
firmware_version="3.0.2",
|
|
source="ble-read-only-status",
|
|
observed_at_utc="2026-08-06T12:00:00Z",
|
|
)
|
|
|
|
restarted, _ = service_with_fake_runtime(tmp_path)
|
|
association_probe = FakeHostWifiAssociationProbe("a" * 64)
|
|
restarted._host_wifi_association_probe = association_probe # type: ignore[assignment] # noqa: SLF001
|
|
capture = _durable_status_capture()
|
|
status_reads: list[tuple[str, bool, str | None]] = []
|
|
pin_calls: list[tuple[object, str]] = []
|
|
network_writes: list[str] = []
|
|
|
|
async def read_current_dhcp_status(
|
|
device_id: str,
|
|
*,
|
|
captured_device: object | None = None,
|
|
recovery_device_session_id: str | None = None,
|
|
allow_known_device_retrieval: bool = False,
|
|
on_gatt_validated: Callable[[object], None] | None = None,
|
|
**_: object,
|
|
) -> dict[str, Any]:
|
|
status_reads.append(
|
|
(device_id, allow_known_device_retrieval, recovery_device_session_id)
|
|
)
|
|
assert captured_device is None
|
|
assert allow_known_device_retrieval is True
|
|
assert on_gatt_validated is not None
|
|
on_gatt_validated(capture)
|
|
return _wifi_status_read("192.168.68.51", device_id=device_id)
|
|
|
|
async def forbidden_network_write(*_: object, **__: object) -> dict[str, Any]:
|
|
network_writes.append("network-write")
|
|
raise AssertionError("stale DHCP recovery must not write to K1")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", read_current_dhcp_status)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_network_write)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"pin_connected_device_handle",
|
|
lambda handle, *, device_session_id: pin_calls.append((handle, device_session_id)),
|
|
)
|
|
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(facade_module, "_host_route_class", lambda _target: "direct-or-routed")
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", lambda _target: True)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_probe_configured_endpoint_host_only",
|
|
lambda target, **_kwargs: facade_module._ConfiguredEndpointHostObservation( # noqa: SLF001
|
|
path=_direct_host_path(target),
|
|
reachable=False,
|
|
reason_code="tcp-connection-timeout",
|
|
),
|
|
)
|
|
|
|
async def bootstrap_after_refresh(
|
|
bound_service: XgridsK1CompatibilityService,
|
|
*,
|
|
parent_operation_id: str | None,
|
|
connection_mode: facade_module.ConnectionMode,
|
|
inspection_only: bool = False,
|
|
) -> None:
|
|
await _synthetic_prestart_control_bootstrap(
|
|
bound_service,
|
|
parent_operation_id=parent_operation_id,
|
|
connection_mode=connection_mode,
|
|
inspection_only=inspection_only,
|
|
)
|
|
|
|
restarted._bootstrap_prestart_control_ready_owned = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
bootstrap_after_refresh,
|
|
restarted,
|
|
)
|
|
|
|
state = asyncio.run(
|
|
restarted.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id=DURABLE_K1_UUID,
|
|
source="durable-configured-state",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert status_reads == [(DURABLE_K1_UUID, True, None)]
|
|
assert network_writes == []
|
|
assert len(pin_calls) == 1
|
|
assert pin_calls[0][0] is capture
|
|
assert state["k1_ip"] == "192.168.68.51"
|
|
assert state["semantic_topology_store"]["record"]["revision"] == 2
|
|
assert state["semantic_topology_store"]["record"]["ipv4"] == "192.168.68.51"
|
|
assert state["connection_verification"]["endpoint_validation"] == (
|
|
"ble-wifi-status-read+mqtt-tcp-connect"
|
|
)
|
|
assert state["connection_verification"]["address_source"] == "ble-wifi-status-read"
|
|
assert state["connection_verification"]["address_changed"] is True
|
|
assert state["connection_verification"]["write_performed"] is False
|
|
assert state["connection_supervisor"]["observed"]["device_identity"]["state"] == (
|
|
"verified"
|
|
)
|
|
assert state["connection_supervisor"]["authority"]["control_allowed"] is True
|
|
|
|
|
|
def test_semantic_durable_verify_rejects_association_change_during_tcp_probe(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
first, _ = service_with_fake_runtime(tmp_path)
|
|
topology_store = first._semantic_topology_store # noqa: SLF001
|
|
assert topology_store is not None
|
|
topology_store.commit(
|
|
transport_ref=DURABLE_K1_UUID,
|
|
connection_mode="bridge",
|
|
ipv4="192.168.68.50",
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
firmware_version="3.0.2",
|
|
source="ble-read-only-status",
|
|
observed_at_utc="2026-08-06T12:00:00Z",
|
|
)
|
|
|
|
restarted, _ = service_with_fake_runtime(tmp_path)
|
|
association_probe = FakeHostWifiAssociationProbe("a" * 64, "b" * 64)
|
|
restarted._host_wifi_association_probe = association_probe # type: ignore[assignment] # noqa: SLF001
|
|
device_edges: list[str] = []
|
|
|
|
async def forbidden_device_edge(*_: object, **__: object) -> dict[str, Any]:
|
|
device_edges.append("device")
|
|
raise AssertionError("association race must fail before BLE or device write")
|
|
|
|
async def forbidden_bootstrap(*_: object, **__: object) -> None:
|
|
device_edges.append("mqtt-bootstrap")
|
|
raise AssertionError("association race must fail before DeviceInfo bootstrap")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_device_edge)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_device_edge)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_probe_control_endpoint_socket",
|
|
lambda _target: facade_module.TcpReachabilityProbeResult(reachable=True),
|
|
)
|
|
restarted._bootstrap_prestart_control_ready_owned = forbidden_bootstrap # type: ignore[method-assign] # noqa: SLF001
|
|
|
|
with pytest.raises(facade_module.ConnectionVerificationError) as raised:
|
|
asyncio.run(
|
|
restarted.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id=DURABLE_K1_UUID,
|
|
source="durable-configured-state",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert raised.value.reason_code == "connection-verify-mqtt-unreachable"
|
|
assert device_edges == []
|
|
assert association_probe.interfaces == ["test0", "test0"]
|
|
state = restarted.state()
|
|
assert state["selected_device_id"] is None
|
|
assert state["active_connection_mode"] is None
|
|
assert state["connection_supervisor"]["intent"] is None
|
|
assert state["connection_supervisor"]["observed"]["endpoint"]["target"] is None
|
|
assert state["semantic_topology_store"]["record"]["revision"] == 1
|
|
|
|
|
|
@pytest.mark.parametrize("observed_session_state", ["ready", "scanning"])
|
|
def test_public_physical_recovery_refreshes_stale_dhcp_then_classifies_without_writes(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
observed_session_state: str,
|
|
) -> None:
|
|
original, _ = service_with_fake_runtime(tmp_path)
|
|
stop_operation_id, compatibility_revision = (
|
|
_persist_resolved_unclassified_stop_for_restart(original)
|
|
)
|
|
topology_store = original._semantic_topology_store # noqa: SLF001
|
|
assert topology_store is not None
|
|
topology_store.commit(
|
|
transport_ref="test-ble-transport",
|
|
connection_mode="bridge",
|
|
ipv4="192.168.68.52",
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
firmware_version="3.0.2",
|
|
source="ble-read-only-status",
|
|
observed_at_utc="2026-08-12T13:28:19.000Z",
|
|
)
|
|
|
|
restarted, runtime = service_with_fake_runtime(tmp_path)
|
|
retirement_permissions: list[bool] = []
|
|
|
|
class TerminalDeviceFaultControl(FakeInteractiveControlSession):
|
|
def retire_for_network_change(
|
|
self,
|
|
*,
|
|
allow_terminal_failure: bool = False,
|
|
**kwargs: object,
|
|
) -> dict[str, object]:
|
|
retirement_permissions.append(allow_terminal_failure)
|
|
if not allow_terminal_failure:
|
|
raise ApplicationAcceptanceError(
|
|
"terminal device fault requires explicit local retirement"
|
|
)
|
|
return super().retire_for_network_change(**kwargs)
|
|
|
|
association_probe = FakeHostWifiAssociationProbe("a" * 64)
|
|
restarted._host_wifi_association_probe = association_probe # type: ignore[assignment] # noqa: SLF001
|
|
capture = _durable_status_capture(device_id="test-ble-transport")
|
|
host_probes: list[str] = []
|
|
status_reads: list[tuple[str, bool, bool, float | None]] = []
|
|
command_edges: list[str] = []
|
|
|
|
def stale_host_probe(
|
|
target: str,
|
|
**_: object,
|
|
) -> facade_module._ConfiguredEndpointHostObservation: # noqa: SLF001
|
|
host_probes.append(target)
|
|
return facade_module._ConfiguredEndpointHostObservation( # noqa: SLF001
|
|
path=_direct_host_path(target),
|
|
reachable=False,
|
|
reason_code="tcp-connection-timeout",
|
|
)
|
|
|
|
async def read_current_dhcp_status(
|
|
device_id: str,
|
|
*,
|
|
allow_known_device_retrieval: bool = False,
|
|
rediscover: bool = False,
|
|
exact_scan_timeout_seconds: float | None = None,
|
|
on_gatt_validated: Callable[[object], None] | None = None,
|
|
**_: object,
|
|
) -> dict[str, Any]:
|
|
status_reads.append(
|
|
(
|
|
device_id,
|
|
allow_known_device_retrieval,
|
|
rediscover,
|
|
exact_scan_timeout_seconds,
|
|
)
|
|
)
|
|
assert on_gatt_validated is not None
|
|
on_gatt_validated(capture)
|
|
return _wifi_status_read("192.168.68.51", device_id=device_id)
|
|
|
|
async def forbidden_network_write(*_: object, **__: object) -> dict[str, Any]:
|
|
command_edges.append("network-write")
|
|
raise AssertionError("physical recovery must not write K1 network state")
|
|
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_probe_configured_endpoint_host_only",
|
|
stale_host_probe,
|
|
)
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", read_current_dhcp_status)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_network_write)
|
|
monkeypatch.setattr(facade_module, "pin_connected_device_handle", lambda *_a, **_k: None)
|
|
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(facade_module, "_host_route_class", lambda _target: "direct-or-routed")
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", lambda _target: True)
|
|
_install_real_coordinator_bootstrap(
|
|
restarted,
|
|
observed_session_state=observed_session_state,
|
|
)
|
|
|
|
before = restarted.state()
|
|
configured_recovery = before["connection_policy"]["actions"][
|
|
"observe-configured-device-network"
|
|
]
|
|
assert configured_recovery["allowed"] is True
|
|
assert configured_recovery["required_transport_ref"] == "test-ble-transport"
|
|
assert configured_recovery["required_connection_mode"] == "bridge"
|
|
assert configured_recovery["requires_live_gatt_validation"] is True
|
|
|
|
restarted._application_control_session = TerminalDeviceFaultControl( # type: ignore[assignment] # noqa: SLF001
|
|
initial_state="failed"
|
|
)
|
|
|
|
verified = asyncio.run(restarted.verify_connection(ConnectionVerifyRequest()))
|
|
|
|
after = restarted._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
assert after is not None
|
|
assert after.operation_id == stop_operation_id
|
|
assert after.revision == compatibility_revision + 1
|
|
assert after.resolved_unclassified_stop_recovery_required is False
|
|
assert after.reconciliations[-1].resolution == (
|
|
"physical-active-observed"
|
|
if observed_session_state == "scanning"
|
|
else "physical-standby-observed"
|
|
)
|
|
assert host_probes == []
|
|
assert status_reads == [
|
|
(
|
|
"test-ble-transport",
|
|
True,
|
|
True,
|
|
facade_module.CONNECTION_VERIFY_EXACT_UUID_SCAN_TIMEOUT_SECONDS,
|
|
)
|
|
]
|
|
assert association_probe.timeout_seconds == [
|
|
facade_module.COMMAND_BOUND_ASSOCIATION_TIMEOUT_SECONDS,
|
|
facade_module.COMMAND_BOUND_ASSOCIATION_TIMEOUT_SECONDS,
|
|
]
|
|
assert command_edges == []
|
|
assert retirement_permissions == [True]
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
assert verified["k1_ip"] == "192.168.68.51"
|
|
assert verified["semantic_topology_store"]["record"]["ipv4"] == "192.168.68.51"
|
|
assert verified["last_operation"]["result"]["write_performed"] is False
|
|
assert verified["last_operation"]["result"]["physical_reconciliation"][
|
|
"performed"
|
|
] is True
|
|
|
|
|
|
def test_durable_restart_rejects_network_revision_race_before_commit_or_pin(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
operation_id = "durable-revision-race"
|
|
first, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_unresolved_network_mutation(
|
|
first,
|
|
operation_id=operation_id,
|
|
transport_ref=DURABLE_K1_UUID,
|
|
)
|
|
restarted, _ = service_with_fake_runtime(tmp_path)
|
|
captured = _durable_status_capture()
|
|
pin_calls: list[tuple[object, str]] = []
|
|
|
|
async def read_while_revision_changes(
|
|
device_id: str,
|
|
*,
|
|
on_gatt_validated: Callable[[object], None] | None = None,
|
|
**_: object,
|
|
) -> dict[str, Any]:
|
|
assert on_gatt_validated is not None
|
|
on_gatt_validated(captured)
|
|
current_ledger = restarted._network_mutation_ledger.snapshot() # noqa: SLF001
|
|
assert current_ledger.record is not None
|
|
restarted._network_mutation_ledger.mark_observing( # noqa: SLF001
|
|
operation_id,
|
|
expected_revision=current_ledger.record.revision,
|
|
write_confirmed=False,
|
|
observation=NetworkStatusEvidence(
|
|
mode="WIFI_CLIENT",
|
|
ipv4="192.168.68.50",
|
|
status_code=1,
|
|
reserved=0,
|
|
),
|
|
)
|
|
return _wifi_status_read("192.168.68.50", device_id=device_id)
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", read_while_revision_changes)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"provision_wifi_once",
|
|
lambda *_args, **_kwargs: (_ for _ in ()).throw(
|
|
AssertionError("revision race must not write")
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"pin_connected_device_handle",
|
|
lambda handle, *, device_session_id: pin_calls.append((handle, device_session_id)),
|
|
)
|
|
|
|
with pytest.raises(facade_module.ConnectionVerificationError) as raised:
|
|
asyncio.run(
|
|
restarted.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id=DURABLE_K1_UUID,
|
|
source="durable-configured-state",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert raised.value.reason_code == "connection-verify-candidate-not-fresh"
|
|
assert pin_calls == []
|
|
state = restarted.state()
|
|
assert state["network_mutation_ledger"]["status"] == "resolved"
|
|
assert state["network_mutation_ledger"]["resolution"] == "interrupted"
|
|
assert state["semantic_topology_store"]["status"] == "empty"
|
|
assert state["selected_device_id"] is None
|
|
assert state["device_session"] is None
|
|
|
|
|
|
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(
|
|
_prepare_request(
|
|
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(
|
|
_connect_request(
|
|
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(
|
|
_prepare_request(
|
|
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")
|
|
|
|
|
|
def test_quick_to_bridge_requires_new_explicit_scan_instead_of_retained_session(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_supervised_connection(
|
|
service,
|
|
target_ipv4=facade_module.AP_FALLBACK_IPV4,
|
|
connection_mode="quick-connect",
|
|
transport_ref="k1-a",
|
|
logical_device_id="device-a",
|
|
)
|
|
service._device_session_id = "quick-session-a" # noqa: SLF001
|
|
service._device_session_opened_at = "2026-08-06T12:00:00Z" # noqa: SLF001
|
|
supervisor_generation = service._connection_supervisor.snapshot().lease.generation # noqa: SLF001
|
|
service._connection_verification = { # noqa: SLF001
|
|
"status": "control-transport-lost",
|
|
"lease_state": "disconnected",
|
|
"lease_generation": supervisor_generation,
|
|
"endpoint_validation": "terminal-mqtt-network-loop",
|
|
"network_reachability": "unreachable",
|
|
"reason_code": "connection_lease_control_transport_lost",
|
|
"observed_at": "2026-08-06T12:01:00Z",
|
|
}
|
|
captured = facade_module.CapturedDiscoveredDevice(
|
|
device=SimpleNamespace(address="k1-a"), # type: ignore[arg-type]
|
|
macos_uuid="k1-a",
|
|
owner_epoch=7,
|
|
)
|
|
provision_calls: list[tuple[object | None, str | None]] = []
|
|
pin_calls: list[tuple[object, str]] = []
|
|
|
|
def recover(device_id: str, *, device_session_id: str) -> object | None:
|
|
assert (device_id, device_session_id) == ("k1-a", "quick-session-a")
|
|
return captured
|
|
|
|
async def provision(
|
|
*_args: object,
|
|
captured_device: object | None = None,
|
|
recovery_device_session_id: str | None = None,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**_kwargs: object,
|
|
) -> dict[str, Any]:
|
|
provision_calls.append((captured_device, recovery_device_session_id))
|
|
_dispatch_test_network_write(on_write_dispatch)
|
|
return {
|
|
"started_at_utc": "2026-08-06T12:02:00Z",
|
|
"completed_at_utc": "2026-08-06T12:02:01Z",
|
|
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
|
"outcome": "lan_address_observed",
|
|
"write_mode": "with_response",
|
|
"baseline_status": {"mode": "WIFI_AP", "ipv4": "192.168.56.1"},
|
|
"observations": [{"status": _wifi_status_read("192.168.68.50")["status"]}],
|
|
}
|
|
|
|
monkeypatch.setattr(facade_module, "connected_device_capture", recover)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"connected_device_recovery_snapshot",
|
|
lambda *_args, **_kwargs: {
|
|
"status": "retained",
|
|
"scope": "owner-epoch-device-session",
|
|
"advertised_now": False,
|
|
"gatt_validated_recently": False,
|
|
},
|
|
)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", provision)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"pin_connected_device_handle",
|
|
lambda handle, *, device_session_id: pin_calls.append((handle, device_session_id)),
|
|
)
|
|
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, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_control_endpoint_reachable",
|
|
lambda _target: True,
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
_reachable_control_endpoint,
|
|
)
|
|
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as raised:
|
|
asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
connection_mode="bridge",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
assert raised.value.reason_code == "network-provision-candidate-not-fresh"
|
|
assert provision_calls == []
|
|
assert pin_calls == []
|
|
|
|
|
|
def test_quick_to_bridge_does_not_retrieve_old_handle_without_new_scan(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_supervised_connection(
|
|
service,
|
|
target_ipv4=facade_module.AP_FALLBACK_IPV4,
|
|
connection_mode="quick-connect",
|
|
transport_ref="k1-a",
|
|
logical_device_id="device-a",
|
|
)
|
|
service._device_session_id = "quick-session-a" # noqa: SLF001
|
|
service._device_session_opened_at = "2026-08-06T12:00:00Z" # noqa: SLF001
|
|
captured = facade_module.CapturedDiscoveredDevice(
|
|
device=SimpleNamespace(address="k1-a"), # type: ignore[arg-type]
|
|
macos_uuid="k1-a",
|
|
owner_epoch=7,
|
|
source="retrieved-session",
|
|
)
|
|
retained_lookups: list[tuple[str, str]] = []
|
|
provision_calls: list[tuple[object | None, str | None]] = []
|
|
|
|
def retained_lookup(device_id: str, *, device_session_id: str) -> object | None:
|
|
retained_lookups.append((device_id, device_session_id))
|
|
# Facade obtains the exact validated object only after the low-level
|
|
# operation resolved/retrieved it inside its BLE arbiter lease.
|
|
return captured
|
|
|
|
async def provision(
|
|
*_args: object,
|
|
captured_device: object | None = None,
|
|
recovery_device_session_id: str | None = None,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**_kwargs: object,
|
|
) -> dict[str, Any]:
|
|
provision_calls.append((captured_device, recovery_device_session_id))
|
|
_dispatch_test_network_write(on_write_dispatch)
|
|
return {
|
|
"started_at_utc": "2026-08-06T12:02:00Z",
|
|
"completed_at_utc": "2026-08-06T12:02:01Z",
|
|
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
|
"outcome": "lan_address_observed",
|
|
"write_mode": "with_response",
|
|
"baseline_status": {"mode": "WIFI_AP", "ipv4": "192.168.56.1"},
|
|
"observations": [{"status": _wifi_status_read("192.168.68.50")["status"]}],
|
|
}
|
|
|
|
monkeypatch.setattr(facade_module, "connected_device_capture", retained_lookup)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"connected_device_recovery_snapshot",
|
|
lambda *_args, **_kwargs: {
|
|
"status": "retained",
|
|
"scope": "owner-epoch-device-session",
|
|
"advertised_now": False,
|
|
"gatt_validated_recently": False,
|
|
},
|
|
)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", provision)
|
|
monkeypatch.setattr(facade_module, "pin_connected_device_handle", lambda *_args, **_kw: None)
|
|
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, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", lambda _target: True)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
_reachable_control_endpoint,
|
|
)
|
|
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as raised:
|
|
asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
connection_mode="bridge",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
assert raised.value.reason_code == "network-provision-candidate-not-fresh"
|
|
assert retained_lookups == []
|
|
assert provision_calls == []
|
|
|
|
|
|
def test_connection_policy_exposes_retained_recovery_without_inventing_presence(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
service._selected_device_id = "k1-a" # noqa: SLF001
|
|
service._connection_mode = "quick-connect" # noqa: SLF001
|
|
service._device_id = "device-a" # noqa: SLF001
|
|
service._device_session_id = "quick-session-a" # noqa: SLF001
|
|
service._device_session_opened_at = "2026-08-06T12:00:00Z" # noqa: SLF001
|
|
service._k1_ip = facade_module.AP_FALLBACK_IPV4 # noqa: SLF001
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"connected_device_recovery_snapshot",
|
|
lambda *_args, **_kwargs: {
|
|
"status": "retained",
|
|
"scope": "owner-epoch-device-session",
|
|
"gatt_validated_recently": True,
|
|
},
|
|
)
|
|
|
|
state = service.state()
|
|
|
|
assert state["devices"] == []
|
|
assert state["current_device_recovery"]["handle_retained"] is True
|
|
assert state["current_device_recovery"]["advertised_now"] is False
|
|
policy = state["connection_policy"]
|
|
assert policy["facts"]["retained_context_is_presence"] is False
|
|
assert policy["actions"]["provision-fresh-device"]["allowed"] is False
|
|
assert policy["actions"]["recover-current-device-network"] == {
|
|
"allowed": True,
|
|
"reason_codes": [],
|
|
"target_source": "retained-current-process",
|
|
"required_transport_ref": "k1-a",
|
|
"requires_live_gatt_validation": True,
|
|
"automatic_retry": False,
|
|
}
|
|
assert policy["recommended_action"] == "recover-current-device-network"
|
|
|
|
|
|
def test_connection_policy_treats_open_durable_audit_as_nonblocking_context(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
_seed_unresolved_network_mutation(service, transport_ref="k1-a")
|
|
|
|
policy = service.state()["connection_policy"]
|
|
|
|
assert policy["facts"]["physical_network_state"] == "unknown"
|
|
assert policy["actions"]["provision-fresh-device"]["allowed"] is True
|
|
assert policy["actions"]["recover-current-device-network"]["allowed"] is False
|
|
assert policy["actions"]["observe-fresh-device-network"]["allowed"] is True
|
|
assert policy["actions"]["observe-fresh-device-network"]["required_transport_ref"] == "k1-a"
|
|
assert policy["recommended_action"] == "observe-fresh-device-network"
|
|
|
|
|
|
def _project_connection_policy_for_test(
|
|
service: XgridsK1CompatibilityService,
|
|
*,
|
|
idempotency: dict[str, object] | None = None,
|
|
idempotency_available: bool = True,
|
|
identity_pins: dict[str, object] | None = None,
|
|
physical_command: dict[str, object] | None = None,
|
|
ble_runtime: dict[str, object] | None = None,
|
|
lifecycle_holders: tuple[str, ...] = (),
|
|
current_device_recovery: dict[str, object] | None = None,
|
|
acquisition_active: bool = False,
|
|
acquisition_state: str | None = None,
|
|
acquisition_cleanup_pending: bool = False,
|
|
runtime_active: bool = False,
|
|
application_control_session: dict[str, object] | None = None,
|
|
) -> dict[str, Any]:
|
|
with service._lock: # noqa: SLF001
|
|
fresh_devices = service._fresh_ble_devices_locked() # noqa: SLF001
|
|
desired_connection_mode = service._desired_connection_mode # noqa: SLF001
|
|
configured_connection_mode = service._connection_mode # noqa: SLF001
|
|
supervisor = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
active_connection_mode = (
|
|
configured_connection_mode if supervisor.authority.control_allowed else None
|
|
)
|
|
return facade_module._connection_policy_projection( # noqa: SLF001
|
|
supervisor=supervisor,
|
|
ledger=service._network_mutation_ledger.snapshot(), # noqa: SLF001
|
|
network_provisioning_idempotency=(
|
|
idempotency
|
|
if idempotency is not None
|
|
else service._network_provisioning_idempotency_public_snapshot() # noqa: SLF001
|
|
),
|
|
network_provisioning_idempotency_available=idempotency_available,
|
|
semantic_topology_store=service._semantic_topology_public_snapshot(), # noqa: SLF001
|
|
device_identity_pin_store=(
|
|
identity_pins
|
|
if identity_pins is not None
|
|
else service._device_identity_pin_public_snapshot() # noqa: SLF001
|
|
),
|
|
physical_command=(
|
|
physical_command
|
|
if physical_command is not None
|
|
else service._physical_command_coordinator.snapshot() # noqa: SLF001
|
|
),
|
|
ble_runtime=(
|
|
ble_runtime
|
|
if ble_runtime is not None
|
|
else {
|
|
"owner_epoch": 1,
|
|
"owner_loop_bound": True,
|
|
"active_operation_kind": None,
|
|
"cleanup_pending": False,
|
|
"poisoned": False,
|
|
}
|
|
),
|
|
lifecycle_process_lease_holders=lifecycle_holders,
|
|
fresh_devices=fresh_devices,
|
|
current_device_recovery=current_device_recovery,
|
|
provisioning_active=False,
|
|
acquisition_active=acquisition_active,
|
|
acquisition_state=acquisition_state,
|
|
acquisition_cleanup_pending=acquisition_cleanup_pending,
|
|
runtime_active=runtime_active,
|
|
application_control_session=(
|
|
application_control_session
|
|
if application_control_session is not None
|
|
else {"state": "idle", "failure": None}
|
|
),
|
|
desired_connection_mode=desired_connection_mode,
|
|
active_connection_mode=active_connection_mode,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("available", "status", "expected_reason"),
|
|
[
|
|
(False, "corrupt", "network-provisioning-idempotency-unavailable"),
|
|
(True, "corrupt", "network-provisioning-idempotency-corrupt"),
|
|
],
|
|
)
|
|
def test_connection_policy_blocks_network_actions_when_idempotency_authority_is_untrusted(
|
|
tmp_path: Path,
|
|
available: bool,
|
|
status: str,
|
|
expected_reason: str,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service)
|
|
|
|
policy = _project_connection_policy_for_test(
|
|
service,
|
|
idempotency={
|
|
"status": status,
|
|
"revision": None,
|
|
"reason_code": expected_reason,
|
|
"active_operation_id": None,
|
|
"active_action": None,
|
|
"active_stage": None,
|
|
"terminal_record_count": 0,
|
|
"mutation_allowed": False,
|
|
},
|
|
idempotency_available=available,
|
|
)
|
|
|
|
assert policy["actions"]["provision-fresh-device"]["allowed"] is False
|
|
assert expected_reason in policy["actions"]["provision-fresh-device"]["reason_codes"]
|
|
assert policy["actions"]["observe-fresh-device-network"]["allowed"] is False
|
|
assert expected_reason in policy["actions"]["observe-fresh-device-network"]["reason_codes"]
|
|
assert policy["actions"]["scan-ble"]["allowed"] is True
|
|
|
|
|
|
def test_connection_policy_blocks_cross_journal_mismatch_before_observation(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
_seed_unresolved_network_mutation(
|
|
service,
|
|
transport_ref="k1-a",
|
|
operation_id="network-ledger-operation",
|
|
)
|
|
|
|
policy = _project_connection_policy_for_test(
|
|
service,
|
|
idempotency={
|
|
"status": "blocked",
|
|
"revision": 3,
|
|
"reason_code": "network-provisioning-idempotency-reconciliation-required",
|
|
"active_operation_id": "different-idempotency-operation",
|
|
"active_action": facade_module.ACTION_NETWORK_PROVISION,
|
|
"active_stage": "unresolved",
|
|
"terminal_record_count": 0,
|
|
"mutation_allowed": False,
|
|
},
|
|
)
|
|
|
|
decision = policy["actions"]["observe-fresh-device-network"]
|
|
assert decision["allowed"] is False
|
|
assert "network-provisioning-idempotency-operation-mismatch" in decision["reason_codes"]
|
|
assert policy["facts"]["network_provisioning_active_operation_matches_ledger"] is False
|
|
|
|
|
|
def test_connection_policy_blocks_identity_dependent_network_control_and_start(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
verify_service, _ = service_with_fake_runtime(tmp_path / "verify")
|
|
_set_scanned_k1(verify_service)
|
|
_seed_supervised_connection(verify_service, with_control=False)
|
|
corrupt_pins = {
|
|
"schema_version": "missioncore.xgrids-k1-device-identity-pins/v1",
|
|
"status": "corrupt",
|
|
"revision": None,
|
|
"pin_count": 0,
|
|
"reason_code": "device-identity-pin-store-corrupt",
|
|
}
|
|
|
|
verify_policy = _project_connection_policy_for_test(
|
|
verify_service,
|
|
identity_pins=corrupt_pins,
|
|
)
|
|
|
|
for action in (
|
|
"provision-fresh-device",
|
|
"observe-fresh-device-network",
|
|
"verify-control-device-info",
|
|
):
|
|
assert verify_policy["actions"][action]["allowed"] is False
|
|
assert (
|
|
"device-identity-pin-store-corrupt" in verify_policy["actions"][action]["reason_codes"]
|
|
)
|
|
|
|
start_service, _ = service_with_fake_runtime(tmp_path / "start")
|
|
_seed_supervised_connection(start_service)
|
|
start_policy = _project_connection_policy_for_test(
|
|
start_service,
|
|
identity_pins=corrupt_pins,
|
|
)
|
|
|
|
assert start_policy["actions"]["start-acquisition"]["allowed"] is False
|
|
assert (
|
|
"device-identity-pin-store-corrupt"
|
|
in start_policy["actions"]["start-acquisition"]["reason_codes"]
|
|
)
|
|
|
|
|
|
def test_connection_policy_blocks_network_observation_when_topology_store_is_corrupt(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_semantic_topology_public_snapshot",
|
|
lambda: {
|
|
"schema_version": "missioncore.xgrids-k1-semantic-topology/v1",
|
|
"status": "corrupt",
|
|
"configured_offline_evidence": False,
|
|
"live_connection_authority": False,
|
|
"reason_code": "semantic-topology-store-corrupt",
|
|
"record": None,
|
|
},
|
|
)
|
|
|
|
policy = _project_connection_policy_for_test(service)
|
|
|
|
assert policy["actions"]["observe-fresh-device-network"]["allowed"] is False
|
|
assert (
|
|
"semantic-topology-store-corrupt"
|
|
in policy["actions"]["observe-fresh-device-network"]["reason_codes"]
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("status", "expected_reason"),
|
|
[
|
|
("unresolved", "physical-command-reconciliation-required"),
|
|
("corrupt", "physical-command-ledger-corrupt"),
|
|
],
|
|
)
|
|
def test_connection_policy_blocks_physical_start_and_stop_on_untrusted_ledger(
|
|
tmp_path: Path,
|
|
status: str,
|
|
expected_reason: str,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(service)
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
assert supervisor.observe_data_plane(
|
|
intent_id=binding.intent_id,
|
|
host_path_epoch=binding.host_path_epoch,
|
|
state="healthy",
|
|
session_id="data-session-a",
|
|
)
|
|
physical_command = {
|
|
"status": status,
|
|
"reason_code": expected_reason,
|
|
"requires_reconciliation": True,
|
|
"automatic_replay_allowed": False,
|
|
"normal_session_recovery_supported": False,
|
|
"recovery_requirement": "explicit-read-only-reconciliation",
|
|
"runtime_bound": False,
|
|
"active_operation_id": None,
|
|
"record": None,
|
|
}
|
|
|
|
policy = _project_connection_policy_for_test(
|
|
service,
|
|
physical_command=physical_command,
|
|
)
|
|
|
|
for action in ("start-acquisition", "stop-acquisition"):
|
|
assert policy["actions"][action]["allowed"] is False
|
|
assert expected_reason in policy["actions"][action]["reason_codes"]
|
|
|
|
|
|
def test_pending_reopened_physical_state_allows_only_read_only_recovery_or_retirement(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
physical = _pending_reopened_physical_snapshot()
|
|
policy = _project_connection_policy_for_test(
|
|
service,
|
|
physical_command=physical,
|
|
)
|
|
|
|
for action in (
|
|
"provision-fresh-device",
|
|
"recover-current-device-network",
|
|
"start-acquisition",
|
|
"stop-acquisition",
|
|
):
|
|
assert policy["actions"][action]["allowed"] is False
|
|
assert (
|
|
"physical-command-reconciliation-required"
|
|
in policy["actions"][action]["reason_codes"]
|
|
)
|
|
read_only = policy["actions"]["observe-fresh-device-network"]
|
|
assert read_only["allowed"] is True
|
|
assert read_only["required_transport_ref"] == "k1-a"
|
|
assert read_only["required_connection_mode"] == "bridge"
|
|
|
|
# A scanning-shaped local snapshot is not STOP authority by itself. The
|
|
# reopened durable row must remain fenced unless the exact adopted worker
|
|
# explicitly exposes its STOP checkpoint.
|
|
no_stop_checkpoint = _project_connection_policy_for_test(
|
|
service,
|
|
physical_command=physical,
|
|
application_control_session={
|
|
"state": "scanning",
|
|
"can_stop": False,
|
|
"failure": None,
|
|
},
|
|
)
|
|
assert no_stop_checkpoint["actions"]["stop-acquisition"]["allowed"] is False
|
|
assert (
|
|
"physical-command-reconciliation-required"
|
|
in no_stop_checkpoint["actions"]["stop-acquisition"]["reason_codes"]
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"snapshot",
|
|
lambda: physical,
|
|
)
|
|
retirement = service._physical_operator_retirement_projection() # noqa: SLF001
|
|
assert retirement["allowed"] is True
|
|
assert retirement["reason_codes"] == []
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("ble_snapshot", "expected_reason"),
|
|
[
|
|
(
|
|
{
|
|
"owner_epoch": 4,
|
|
"owner_loop_bound": False,
|
|
"active_operation_kind": "scan",
|
|
"cleanup_pending": True,
|
|
"poisoned": False,
|
|
},
|
|
"ble-runtime-cleanup-pending",
|
|
),
|
|
(
|
|
{
|
|
"owner_epoch": 5,
|
|
"owner_loop_bound": False,
|
|
"active_operation_kind": "status-read",
|
|
"cleanup_pending": True,
|
|
"poisoned": True,
|
|
},
|
|
"ble-runtime-restart-required",
|
|
),
|
|
],
|
|
)
|
|
def test_connection_policy_blocks_control_and_physical_edges_on_ble_quarantine(
|
|
tmp_path: Path,
|
|
ble_snapshot: dict[str, object],
|
|
expected_reason: str,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_supervised_connection(service)
|
|
|
|
policy = _project_connection_policy_for_test(
|
|
service,
|
|
ble_runtime=ble_snapshot,
|
|
)
|
|
|
|
assert policy["actions"]["start-acquisition"]["allowed"] is False
|
|
assert expected_reason in policy["actions"]["start-acquisition"]["reason_codes"]
|
|
|
|
|
|
def test_state_projects_ble_runtime_quarantine_into_composite_policy(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
poisoned = {
|
|
"owner_epoch": 8,
|
|
"owner_loop_bound": False,
|
|
"active_operation_kind": "status-read",
|
|
"cleanup_pending": True,
|
|
"poisoned": True,
|
|
}
|
|
monkeypatch.setattr(facade_module, "ble_runtime_snapshot", lambda: poisoned)
|
|
|
|
state = service.state()
|
|
|
|
assert state["ble_runtime"] == poisoned
|
|
assert state["connection_policy"]["facts"]["ble_runtime"] == {
|
|
"active_operation_kind": "status-read",
|
|
"cleanup_pending": True,
|
|
"poisoned": True,
|
|
}
|
|
assert (
|
|
"ble-runtime-restart-required"
|
|
in state["connection_policy"]["actions"]["scan-ble"]["reason_codes"]
|
|
)
|
|
|
|
|
|
def test_connection_policy_blocks_physical_edges_while_network_owns_process_lease(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(service)
|
|
assert service._connection_supervisor.observe_data_plane( # noqa: SLF001
|
|
intent_id=binding.intent_id,
|
|
host_path_epoch=binding.host_path_epoch,
|
|
state="healthy",
|
|
session_id="data-session-a",
|
|
)
|
|
|
|
policy = _project_connection_policy_for_test(
|
|
service,
|
|
lifecycle_holders=("network",),
|
|
)
|
|
|
|
for action in ("start-acquisition", "stop-acquisition"):
|
|
assert policy["actions"][action]["allowed"] is False
|
|
assert (
|
|
"k1-lifecycle-process-lease-network-owned" in policy["actions"][action]["reason_codes"]
|
|
)
|
|
|
|
|
|
def test_connection_policy_keeps_host_tcp_probe_independent_of_other_failures(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_supervised_connection(
|
|
service,
|
|
with_control=False,
|
|
endpoint_reachable=False,
|
|
)
|
|
|
|
policy = _project_connection_policy_for_test(
|
|
service,
|
|
idempotency={
|
|
"status": "corrupt",
|
|
"active_operation_id": None,
|
|
},
|
|
idempotency_available=False,
|
|
identity_pins={"status": "corrupt"},
|
|
physical_command={
|
|
"status": "corrupt",
|
|
"requires_reconciliation": True,
|
|
},
|
|
ble_runtime={
|
|
"owner_epoch": 9,
|
|
"owner_loop_bound": False,
|
|
"active_operation_kind": "scan",
|
|
"cleanup_pending": True,
|
|
"poisoned": True,
|
|
},
|
|
lifecycle_holders=("network",),
|
|
)
|
|
|
|
assert policy["actions"]["probe-endpoint"]["allowed"] is True
|
|
assert policy["actions"]["verify-control-device-info"]["allowed"] is False
|
|
assert policy["actions"]["start-acquisition"]["allowed"] is False
|
|
|
|
|
|
def test_connection_policy_data_loss_preserves_exact_stop_and_falls_back_to_local_only(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(service)
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
assert supervisor.observe_data_plane(
|
|
intent_id=binding.intent_id,
|
|
host_path_epoch=binding.host_path_epoch,
|
|
state="healthy",
|
|
session_id="data-session-a",
|
|
)
|
|
assert supervisor.observe_data_plane(
|
|
intent_id=binding.intent_id,
|
|
host_path_epoch=binding.host_path_epoch,
|
|
state="lost",
|
|
session_id="data-session-a",
|
|
reason_code="test-data-loss",
|
|
)
|
|
|
|
healthy_control = _project_connection_policy_for_test(
|
|
service,
|
|
acquisition_active=True,
|
|
runtime_active=True,
|
|
)
|
|
|
|
assert healthy_control["actions"]["acknowledge-data-loss"]["allowed"] is False
|
|
assert healthy_control["actions"]["stop-acquisition"]["allowed"] is True
|
|
assert healthy_control["actions"]["stop-local-receiver"]["allowed"] is True
|
|
assert healthy_control["actions"]["stop-local-receiver"]["physical_command_allowed"] is False
|
|
assert healthy_control["actions"]["stop-local-receiver"]["physical_outcome"] == ("unknown")
|
|
assert healthy_control["recommended_action"] == "stop-acquisition"
|
|
|
|
assert supervisor.observe_control_loss(
|
|
intent_id=binding.intent_id,
|
|
host_path_epoch=binding.host_path_epoch,
|
|
control_session_id="test-control-test-ble-transport",
|
|
reason_code="test-control-loss",
|
|
)
|
|
control_lost = _project_connection_policy_for_test(
|
|
service,
|
|
acquisition_active=True,
|
|
runtime_active=True,
|
|
)
|
|
|
|
assert control_lost["actions"]["stop-acquisition"]["allowed"] is False
|
|
assert (
|
|
"physical-control-authority-unavailable"
|
|
in control_lost["actions"]["stop-acquisition"]["reason_codes"]
|
|
)
|
|
assert control_lost["actions"]["stop-local-receiver"]["allowed"] is True
|
|
assert control_lost["actions"]["stop-local-receiver"]["execution_mode"] == ("capture-only")
|
|
assert control_lost["actions"]["stop-local-receiver"]["physical_command_allowed"] is False
|
|
assert control_lost["recommended_action"] == "stop-local-receiver"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("acquisition_active", "cleanup_pending", "runtime_active", "expected_allowed"),
|
|
[
|
|
(True, False, False, True),
|
|
(False, True, False, True),
|
|
(False, False, True, True),
|
|
(False, False, False, False),
|
|
],
|
|
)
|
|
def test_connection_policy_local_receiver_cleanup_uses_independent_local_liveness(
|
|
tmp_path: Path,
|
|
acquisition_active: bool,
|
|
cleanup_pending: bool,
|
|
runtime_active: bool,
|
|
expected_allowed: bool,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_supervised_connection(service)
|
|
|
|
policy = _project_connection_policy_for_test(
|
|
service,
|
|
acquisition_active=acquisition_active,
|
|
acquisition_cleanup_pending=cleanup_pending,
|
|
runtime_active=runtime_active,
|
|
)
|
|
|
|
local_cleanup = policy["actions"]["stop-local-receiver"]
|
|
assert local_cleanup["allowed"] is expected_allowed
|
|
assert local_cleanup["execution_mode"] == "capture-only"
|
|
assert local_cleanup["physical_command_allowed"] is False
|
|
assert local_cleanup["physical_outcome"] == "unknown"
|
|
if expected_allowed:
|
|
assert local_cleanup["reason_codes"] == []
|
|
else:
|
|
assert local_cleanup["reason_codes"] == ["local-acquisition-receiver-not-active"]
|
|
|
|
# Local cleanup eligibility never broadens the separately gated physical STOP.
|
|
assert policy["actions"]["stop-acquisition"]["allowed"] is False
|
|
|
|
|
|
def test_connection_policy_current_target_allows_host_probe_not_ble_refresh(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(service)
|
|
assert service._connection_supervisor.observe_control_loss( # noqa: SLF001
|
|
intent_id=binding.intent_id,
|
|
host_path_epoch=binding.host_path_epoch,
|
|
control_session_id="test-control-test-ble-transport",
|
|
reason_code="test-control-loss",
|
|
)
|
|
|
|
policy = _project_connection_policy_for_test(
|
|
service,
|
|
current_device_recovery=None,
|
|
)
|
|
|
|
assert service._connection_supervisor.snapshot().last_known is not None # noqa: SLF001
|
|
assert policy["actions"]["inspect-configured-endpoint"]["allowed"] is True
|
|
assert (
|
|
policy["actions"]["inspect-configured-endpoint"]["requires_live_gatt_validation"] is False
|
|
)
|
|
assert policy["actions"]["inspect-configured-endpoint"]["target_source"] == (
|
|
"configured-topology"
|
|
)
|
|
assert policy["actions"]["inspect-configured-endpoint"]["required_transport_ref"] is None
|
|
assert policy["actions"]["observe-fresh-device-network"]["allowed"] is False
|
|
assert (
|
|
"fresh-ble-candidate-required"
|
|
in policy["actions"]["observe-fresh-device-network"]["reason_codes"]
|
|
)
|
|
|
|
|
|
def test_configured_endpoint_probe_uses_durable_topology_after_restart_without_ble(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
first, _ = service_with_fake_runtime(tmp_path)
|
|
topology_store = first._semantic_topology_store # noqa: SLF001
|
|
assert topology_store is not None
|
|
topology_store.commit(
|
|
transport_ref="durable-k1-transport",
|
|
connection_mode="bridge",
|
|
ipv4="192.168.1.20",
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
firmware_version="3.0.2",
|
|
source="ble-read-only-status",
|
|
observed_at_utc="2026-08-06T12:00:00Z",
|
|
)
|
|
|
|
restarted, _ = service_with_fake_runtime(tmp_path)
|
|
ble_calls: list[str] = []
|
|
|
|
async def forbidden_ble(*_: object, **__: object) -> dict[str, Any]:
|
|
ble_calls.append("ble")
|
|
raise AssertionError("configured endpoint probe must not enter BLE")
|
|
|
|
def forbidden_ble_lease(*_: object, **__: object) -> object:
|
|
ble_calls.append("ble-lease")
|
|
raise AssertionError("configured endpoint probe must not borrow the BLE lease")
|
|
|
|
def forbidden_authoritative_probe(*_: object, **__: object) -> object:
|
|
raise AssertionError("configured endpoint probe must not update the supervisor")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_ble)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_ble)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"borrow_ble_runtime_process_lease",
|
|
forbidden_ble_lease,
|
|
)
|
|
monkeypatch.setattr(restarted, "_probe_control_endpoint", forbidden_authoritative_probe)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_probe_control_endpoint_socket",
|
|
lambda target: facade_module.TcpReachabilityProbeResult(
|
|
reachable=target == "192.168.1.20",
|
|
reason_code=(None if target == "192.168.1.20" else "tcp-endpoint-unreachable"),
|
|
),
|
|
)
|
|
|
|
before = restarted.state()
|
|
assert before["selected_device_id"] is None
|
|
assert before["connection_policy"]["actions"]["inspect-configured-endpoint"]["allowed"] is True
|
|
assert (
|
|
before["connection_policy"]["actions"]["inspect-configured-endpoint"]["target_source"]
|
|
== "configured-topology"
|
|
)
|
|
assert (
|
|
before["connection_policy"]["actions"]["inspect-configured-endpoint"][
|
|
"required_transport_ref"
|
|
]
|
|
is None
|
|
)
|
|
assert (
|
|
before["connection_policy"]["actions"]["observe-fresh-device-network"]["allowed"] is False
|
|
)
|
|
|
|
state = asyncio.run(
|
|
restarted.probe_configured_endpoint(
|
|
ConfiguredEndpointProbeRequest(
|
|
operation_id="op-00000000-0000-4000-8000-000000000651",
|
|
)
|
|
)
|
|
)
|
|
|
|
assert ble_calls == []
|
|
assert state["selected_device_id"] is None
|
|
assert state["connection_verification"]["status"] == "not-probed"
|
|
assert state["configured_endpoint_probe"] == {
|
|
"schema_version": "missioncore.xgrids-k1-configured-endpoint-probe/v1",
|
|
"status": "reachable",
|
|
"target_source": "durable-semantic-topology",
|
|
"connection_mode": "bridge",
|
|
"endpoint": "192.168.1.20",
|
|
"transport_ref": "durable-k1-transport",
|
|
"intent_id": None,
|
|
"semantic_revision": 1,
|
|
"host_route_available": True,
|
|
"host_route_class": "direct",
|
|
"tcp_reachable": True,
|
|
"identity_validation": "not-performed",
|
|
"control_authority_granted": False,
|
|
"ble_operation_performed": False,
|
|
"network_mutation_performed": False,
|
|
"automatic_retry": False,
|
|
"observed_at": state["configured_endpoint_probe"]["observed_at"],
|
|
"reason_code": None,
|
|
}
|
|
assert state["connection_supervisor"]["authority"]["control_allowed"] is False
|
|
assert state["last_operation"]["action"] == "connection.endpoint-probe"
|
|
assert state["last_operation"]["status"] == "succeeded"
|
|
assert state["last_operation"]["result"]["identity_validation"] == "not-performed"
|
|
assert state["last_operation"]["result"]["network_mutation_performed"] is False
|
|
|
|
|
|
def test_configured_endpoint_probe_reports_unreachable_without_promoting_authority(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
topology_store = service._semantic_topology_store # noqa: SLF001
|
|
assert topology_store is not None
|
|
topology_store.commit(
|
|
transport_ref="durable-k1-transport",
|
|
connection_mode="quick-connect",
|
|
ipv4="192.168.56.1",
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
firmware_version="3.0.2",
|
|
source="ble-read-only-status",
|
|
observed_at_utc="2026-08-06T12:00:00Z",
|
|
)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_probe_control_endpoint_socket",
|
|
lambda _target: facade_module.TcpReachabilityProbeResult(
|
|
reachable=False,
|
|
reason_code="tcp-endpoint-unreachable",
|
|
),
|
|
)
|
|
|
|
state = asyncio.run(service.probe_configured_endpoint())
|
|
|
|
probe = state["configured_endpoint_probe"]
|
|
assert probe["status"] == "endpoint-unreachable"
|
|
assert probe["host_route_available"] is True
|
|
assert probe["tcp_reachable"] is False
|
|
assert probe["reason_code"] == "tcp-endpoint-unreachable"
|
|
assert probe["control_authority_granted"] is False
|
|
assert state["connection_supervisor"]["authority"]["control_allowed"] is False
|
|
|
|
|
|
def test_configured_endpoint_probe_prefers_current_supervisor_target(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
topology_store = service._semantic_topology_store # noqa: SLF001
|
|
assert topology_store is not None
|
|
topology_store.commit(
|
|
transport_ref="older-durable-transport",
|
|
connection_mode="bridge",
|
|
ipv4="192.168.1.99",
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
firmware_version="3.0.2",
|
|
source="ble-read-only-status",
|
|
observed_at_utc="2026-08-06T12:00:00Z",
|
|
)
|
|
_seed_supervised_connection(
|
|
service,
|
|
target_ipv4="192.168.1.20",
|
|
with_control=False,
|
|
)
|
|
probed_targets: list[str] = []
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_probe_control_endpoint_socket",
|
|
lambda target: (
|
|
probed_targets.append(target)
|
|
or facade_module.TcpReachabilityProbeResult(reachable=True)
|
|
),
|
|
)
|
|
|
|
state = asyncio.run(service.probe_configured_endpoint())
|
|
|
|
assert probed_targets == ["192.168.1.20"]
|
|
assert state["configured_endpoint_probe"]["target_source"] == "current-supervisor"
|
|
assert state["configured_endpoint_probe"]["endpoint"] == "192.168.1.20"
|
|
assert state["configured_endpoint_probe"]["transport_ref"] == "test-ble-transport"
|
|
assert state["configured_endpoint_probe"]["intent_id"] is not None
|
|
assert state["configured_endpoint_probe"]["semantic_revision"] is None
|
|
assert state["configured_endpoint_probe"]["identity_validation"] == "not-performed"
|
|
|
|
|
|
def test_configured_endpoint_probe_requires_current_or_durable_target(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
|
|
with pytest.raises(
|
|
facade_module.ConfiguredEndpointProbeError,
|
|
match="Нет сохранённого endpoint",
|
|
) as raised:
|
|
asyncio.run(service.probe_configured_endpoint())
|
|
|
|
assert raised.value.reason_code == "configured-endpoint-unavailable"
|
|
operation = service.state()["last_operation"]
|
|
assert operation["action"] == "connection.endpoint-probe"
|
|
assert operation["status"] == "failed"
|
|
assert operation["error"]["side_effect_status"] == "none"
|
|
assert operation["error"]["safe_to_retry"] is True
|
|
|
|
|
|
def test_configured_endpoint_probe_maps_untrusted_store_to_action_scoped_error(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
service._semantic_topology_store = None # noqa: SLF001
|
|
|
|
with pytest.raises(facade_module.ConfiguredEndpointProbeError) as raised:
|
|
asyncio.run(service.probe_configured_endpoint())
|
|
|
|
assert raised.value.reason_code == "configured-endpoint-topology-corrupt"
|
|
operation = service.state()["last_operation"]
|
|
assert operation["action"] == "connection.endpoint-probe"
|
|
assert operation["status"] == "failed"
|
|
assert operation["error"]["side_effect_status"] == "none"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
(
|
|
"previous_mode",
|
|
"previous_session_id",
|
|
"previous_ipv4",
|
|
"requested_mode",
|
|
"requested_ssid",
|
|
"requested_password",
|
|
"requested_attestation",
|
|
),
|
|
[
|
|
pytest.param(
|
|
"quick-connect",
|
|
"quick-session-a",
|
|
facade_module.AP_FALLBACK_IPV4,
|
|
"bridge",
|
|
"lab-router",
|
|
SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
ATTESTATION,
|
|
id="quick-to-bridge",
|
|
),
|
|
pytest.param(
|
|
"bridge",
|
|
"bridge-session-a",
|
|
"192.168.68.50",
|
|
"quick-connect",
|
|
None,
|
|
None,
|
|
QUICK_CONNECT_ATTESTATION,
|
|
id="bridge-to-quick-connect",
|
|
),
|
|
],
|
|
)
|
|
def test_fresh_cross_mode_prewrite_failure_leaves_clean_unselected_state(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
previous_mode: str,
|
|
previous_session_id: str,
|
|
previous_ipv4: str,
|
|
requested_mode: str,
|
|
requested_ssid: str | None,
|
|
requested_password: SecretStr | None,
|
|
requested_attestation: CompatibilityAttestationRequest,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
service._selected_device_id = "k1-a" # noqa: SLF001
|
|
service._connection_mode = previous_mode # type: ignore[assignment] # noqa: SLF001
|
|
service._device_id = "device-a" # noqa: SLF001
|
|
service._device_session_id = previous_session_id # noqa: SLF001
|
|
service._device_session_opened_at = "2026-08-06T12:00:00Z" # noqa: SLF001
|
|
service._device_session_discovery_generation = 0 # noqa: SLF001
|
|
service._k1_ip = previous_ipv4 # noqa: SLF001
|
|
mode_revision = _select_connection_mode(service, requested_mode)
|
|
service._ble_discovery_generation += 1 # noqa: SLF001
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
captured = facade_module.CapturedDiscoveredDevice(
|
|
device=SimpleNamespace(address="k1-a"), # type: ignore[arg-type]
|
|
macos_uuid="k1-a",
|
|
owner_epoch=7,
|
|
scan_generation=service._ble_discovery_generation, # noqa: SLF001
|
|
)
|
|
prewrite_attempts: list[tuple[str, object | None]] = []
|
|
|
|
def prewrite_failure(captured_device: object | None) -> RuntimeError:
|
|
prewrite_attempts.append((requested_mode, captured_device))
|
|
error = RuntimeError("powered off after fresh advertisement")
|
|
error.operation_stage = "connect" # type: ignore[attr-defined]
|
|
error.device_write_attempted = False # type: ignore[attr-defined]
|
|
error.device_write_confirmed = False # type: ignore[attr-defined]
|
|
return error
|
|
|
|
async def bridge_powered_off_before_write(
|
|
*_args: object,
|
|
captured_device: object | None = None,
|
|
**_kwargs: object,
|
|
) -> dict[str, Any]:
|
|
raise prewrite_failure(captured_device)
|
|
|
|
@asynccontextmanager
|
|
async def quick_powered_off_before_write(
|
|
*_args: object,
|
|
captured_device: object | None = None,
|
|
**_kwargs: object,
|
|
) -> AsyncIterator[dict[str, Any]]:
|
|
raise prewrite_failure(captured_device)
|
|
yield {}
|
|
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_capture_network_intent_device",
|
|
lambda _device_id: captured,
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"connected_device_capture",
|
|
lambda *_args, **_kwargs: None,
|
|
)
|
|
if requested_mode == "quick-connect":
|
|
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",
|
|
},
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"device_ap_activation_session",
|
|
quick_powered_off_before_write,
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"provision_wifi_once",
|
|
lambda *_args, **_kwargs: (_ for _ in ()).throw(
|
|
AssertionError("Quick Connect must not use the Bridge provisioning write")
|
|
),
|
|
)
|
|
else:
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"provision_wifi_once",
|
|
bridge_powered_off_before_write,
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"device_ap_activation_session",
|
|
lambda *_args, **_kwargs: (_ for _ in ()).throw(
|
|
AssertionError("Bridge must not use the Quick Connect AP write")
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"pin_connected_device_handle",
|
|
lambda *_args, **_kwargs: (_ for _ in ()).throw(
|
|
AssertionError("a failed pre-write recovery must not promote a new session")
|
|
),
|
|
)
|
|
|
|
with pytest.raises(RuntimeError, match="powered off after fresh advertisement"):
|
|
asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid=requested_ssid,
|
|
password=requested_password,
|
|
connection_mode=requested_mode,
|
|
compatibility_attestation=requested_attestation,
|
|
expected_mode_revision=mode_revision,
|
|
expected_discovery_generation=(
|
|
service._ble_discovery_generation # noqa: SLF001
|
|
),
|
|
)
|
|
)
|
|
)
|
|
|
|
assert prewrite_attempts == [(requested_mode, captured)]
|
|
assert service._selected_device_id is None # noqa: SLF001
|
|
assert service._connection_mode is None # noqa: SLF001
|
|
assert service._device_id == "device-a" # noqa: SLF001
|
|
assert service._device_session_id is None # noqa: SLF001
|
|
assert service._k1_ip is None # noqa: SLF001
|
|
state = service.state()
|
|
assert state["current_device_recovery"] is None
|
|
operation = next(item for item in state["operations"] if item["action"] == "network.provision")
|
|
assert operation["error"]["safe_to_retry"] is True
|
|
assert operation["error"]["side_effect_status"] == "none"
|
|
assert operation["error"]["operation_stage"] == "connect"
|
|
assert state["network_mutation_ledger"]["status"] == "empty"
|
|
assert state["connection_policy"]["actions"]["provision-fresh-device"]["allowed"] is True
|
|
|
|
|
|
def test_same_uuid_rediscovery_is_candidate_only_and_never_resurrects_session(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
service._selected_device_id = "k1-a" # noqa: SLF001
|
|
service._connection_mode = "bridge" # noqa: SLF001
|
|
service._device_id = "device-a" # noqa: SLF001
|
|
service._device_session_id = "ended-session-a" # noqa: SLF001
|
|
service._device_session_opened_at = "2026-08-06T12:00:00Z" # noqa: SLF001
|
|
service._device_session_discovery_generation = 0 # noqa: SLF001
|
|
service._k1_ip = "192.168.68.50" # noqa: SLF001
|
|
invalidations: list[tuple[str, str]] = []
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"invalidate_connected_device_session",
|
|
lambda device_id, *, device_session_id: (
|
|
invalidations.append((device_id, device_session_id)) or True
|
|
),
|
|
)
|
|
|
|
service._retire_ephemeral_device_binding_for_new_intent() # noqa: SLF001
|
|
assert invalidations == [("k1-a", "ended-session-a")]
|
|
|
|
discovery_calls: list[float] = []
|
|
gatt_or_session_edges: list[str] = []
|
|
|
|
async def rediscover_same_uuid(
|
|
duration_seconds: float,
|
|
*,
|
|
on_admitted: Callable[[], None] | None = None,
|
|
) -> dict[str, Any]:
|
|
discovery_calls.append(duration_seconds)
|
|
assert on_admitted is not None
|
|
on_admitted()
|
|
return _ble_scan_result("k1-a")
|
|
|
|
def forbidden_edge(*_args: object, **_kwargs: object) -> None:
|
|
gatt_or_session_edges.append("unexpected")
|
|
raise AssertionError("rediscovery must remain candidate-only until explicit Connect")
|
|
|
|
monkeypatch.setattr(facade_module, "scan", rediscover_same_uuid)
|
|
monkeypatch.setattr(facade_module, "_capture_network_intent_device", forbidden_edge)
|
|
monkeypatch.setattr(facade_module, "connected_device_capture", forbidden_edge)
|
|
monkeypatch.setattr(facade_module, "pin_connected_device_handle", forbidden_edge)
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_edge)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_edge)
|
|
monkeypatch.setattr(facade_module, "device_ap_activation_session", forbidden_edge)
|
|
|
|
rediscovered = asyncio.run(service.scan_ble(BleScanRequest(duration_seconds=1.0)))
|
|
|
|
assert discovery_calls == [1.0]
|
|
assert gatt_or_session_edges == []
|
|
assert rediscovered["devices"] == [
|
|
{
|
|
"device_id": "k1-a",
|
|
"name": "XGR-K1",
|
|
"rssi": -44,
|
|
"address": None,
|
|
"connectable": None,
|
|
"likely_k1": True,
|
|
}
|
|
]
|
|
assert rediscovered["selected_device_id"] is None
|
|
assert rediscovered["ble_discovery_generation"] == 1
|
|
assert rediscovered["device_session"] is None
|
|
assert rediscovered["current_device_recovery"] is None
|
|
assert rediscovered["connection_mode"] is None
|
|
assert rediscovered["k1_ip"] is None
|
|
assert rediscovered["connection_policy"]["actions"]["provision-fresh-device"]["allowed"] is True
|
|
|
|
|
|
def test_quick_to_bridge_recovery_rejects_nonterminal_control_before_retirement(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
service._selected_device_id = "k1-a" # noqa: SLF001
|
|
service._connection_mode = "quick-connect" # noqa: SLF001
|
|
service._device_session_id = "quick-session-a" # noqa: SLF001
|
|
service._k1_ip = facade_module.AP_FALLBACK_IPV4 # noqa: SLF001
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
captured = facade_module.CapturedDiscoveredDevice(
|
|
device=SimpleNamespace(address="k1-a"), # type: ignore[arg-type]
|
|
macos_uuid="k1-a",
|
|
owner_epoch=7,
|
|
)
|
|
|
|
class BusyControlSession:
|
|
def snapshot(self) -> dict[str, object]:
|
|
return {"state": "scanning", "failure": None}
|
|
|
|
def retire_for_network_change(self, **_: object) -> dict[str, object]:
|
|
raise AssertionError("recovery admission must fail before retirement")
|
|
|
|
service._application_control_session = BusyControlSession() # type: ignore[assignment] # noqa: SLF001
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_capture_network_intent_device",
|
|
lambda *_args, **_kwargs: captured,
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"connected_device_capture",
|
|
lambda *_args, **_kwargs: captured,
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"connected_device_recovery_snapshot",
|
|
lambda *_args, **_kwargs: {
|
|
"status": "retained",
|
|
"scope": "owner-epoch-device-session",
|
|
"advertised_now": False,
|
|
"gatt_validated_recently": False,
|
|
},
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"provision_wifi_once",
|
|
lambda *_args, **_kwargs: (_ for _ in ()).throw(
|
|
AssertionError("rejected recovery must not reach BLE")
|
|
),
|
|
)
|
|
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as raised:
|
|
asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
connection_mode="bridge",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
assert raised.value.reason_code == "network-provision-recovery-control-not-admissible"
|
|
assert service._connection_mode == "quick-connect" # noqa: SLF001
|
|
assert service._device_session_id == "quick-session-a" # noqa: SLF001
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"failure_reason",
|
|
["mqtt_network_loop_failed", "application-connection-binding-lost"],
|
|
)
|
|
def test_terminal_control_loss_retires_ephemeral_device_session_without_retry(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
failure_reason: str,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_supervised_connection(
|
|
service,
|
|
target_ipv4=facade_module.AP_FALLBACK_IPV4,
|
|
connection_mode="quick-connect",
|
|
transport_ref="k1-a",
|
|
logical_device_id="device-a",
|
|
)
|
|
service._device_session_id = "quick-session-a" # noqa: SLF001
|
|
service._device_session_opened_at = "2026-08-06T12:00:00Z" # noqa: SLF001
|
|
supervisor_generation = service._connection_supervisor.snapshot().lease.generation # noqa: SLF001
|
|
service._connection_verification = { # noqa: SLF001
|
|
"status": "reachable",
|
|
"lease_state": "reachable",
|
|
"lease_generation": supervisor_generation,
|
|
"endpoint_validation": "provisioning-status+mqtt-tcp-connect",
|
|
"network_reachability": "reachable",
|
|
"write_performed": True,
|
|
"observed_at": "2026-08-06T12:00:00Z",
|
|
}
|
|
|
|
class FailedControlSession:
|
|
retire_calls = 0
|
|
retired = False
|
|
|
|
def snapshot(self) -> dict[str, object]:
|
|
if self.retired:
|
|
return {"state": "idle", "can_open": True, "failure": None}
|
|
return {
|
|
"state": "failed",
|
|
"failure": {
|
|
"reason_code": failure_reason,
|
|
"failed_phase": "maintain-open",
|
|
},
|
|
}
|
|
|
|
def retire_for_network_change(self, **_: object) -> dict[str, object]:
|
|
self.retire_calls += 1
|
|
if self.retire_calls == 1:
|
|
raise RuntimeError("control worker is still retiring")
|
|
self.retired = True
|
|
return {"state": "idle", "can_open": True}
|
|
|
|
service._application_control_session = FailedControlSession() # type: ignore[assignment] # noqa: SLF001
|
|
invalidations: list[bool] = []
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"invalidate_connected_device_session",
|
|
lambda *_args, **_kwargs: invalidations.append(True) or True,
|
|
)
|
|
|
|
state = service.state()
|
|
|
|
assert invalidations == [True]
|
|
assert state["k1_ip"] is None
|
|
assert state["connection_mode"] is None
|
|
assert state["device_session"] is None
|
|
assert state["current_device_recovery"] is None
|
|
assert service._selected_device_id is None # noqa: SLF001
|
|
assert state["connection_supervisor"]["authority"]["control_allowed"] is False
|
|
assert state["connection_supervisor"]["authority"]["acquisition_start_allowed"] is False
|
|
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
fresh = service.state()
|
|
assert service._application_control_session.retire_calls == 2 # type: ignore[attr-defined] # noqa: SLF001
|
|
assert fresh["connection_policy"]["actions"]["provision-fresh-device"]["allowed"] is True
|
|
|
|
|
|
def test_configured_unverified_endpoint_loss_retires_ephemeral_session(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_supervised_connection(
|
|
service,
|
|
target_ipv4="192.168.68.50",
|
|
connection_mode="bridge",
|
|
transport_ref="k1-a",
|
|
logical_device_id="device-a",
|
|
with_control=False,
|
|
)
|
|
assert service._connection_supervisor.snapshot().last_known is None # noqa: SLF001
|
|
invalidations: list[bool] = []
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"invalidate_connected_device_session",
|
|
lambda *_args, **_kwargs: invalidations.append(True) or True,
|
|
)
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
snapshot = supervisor.snapshot()
|
|
assert snapshot.intent is not None
|
|
assert snapshot.endpoint.target is not None
|
|
|
|
for attempt in range(3):
|
|
accepted = supervisor.observe_endpoint(
|
|
target=snapshot.endpoint.target,
|
|
intent_id=snapshot.intent.intent_id,
|
|
host_path_epoch=snapshot.host_path.epoch,
|
|
reachable=False,
|
|
reason_code="k1-control-port-unreachable",
|
|
)
|
|
assert accepted is True
|
|
state = service.state()
|
|
if attempt < 2:
|
|
assert state["device_session"] is not None
|
|
|
|
assert invalidations == [True]
|
|
assert state["device_session"] is None
|
|
assert state["connection_mode"] is None
|
|
assert state["k1_ip"] is None
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
fresh = service.state()
|
|
assert fresh["connection_policy"]["actions"]["provision-fresh-device"]["allowed"] is True
|
|
|
|
|
|
def test_endpoint_loss_preserves_same_generation_fresh_candidate_for_reprovision(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_supervised_connection(
|
|
service,
|
|
target_ipv4="192.168.68.50",
|
|
connection_mode="bridge",
|
|
transport_ref="k1-a",
|
|
logical_device_id="device-a",
|
|
with_control=False,
|
|
)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
discovery_generation = service._ble_discovery_generation # noqa: SLF001
|
|
invalidations: list[tuple[str, str]] = []
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"invalidate_connected_device_session",
|
|
lambda device_id, *, device_session_id: (
|
|
invalidations.append((device_id, device_session_id)) or True
|
|
),
|
|
)
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
snapshot = supervisor.snapshot()
|
|
assert snapshot.intent is not None
|
|
assert snapshot.endpoint.target is not None
|
|
|
|
for _ in range(3):
|
|
accepted = supervisor.observe_endpoint(
|
|
target=snapshot.endpoint.target,
|
|
intent_id=snapshot.intent.intent_id,
|
|
host_path_epoch=snapshot.host_path.epoch,
|
|
reachable=False,
|
|
reason_code="k1-control-port-unreachable",
|
|
)
|
|
assert accepted is True
|
|
state = service.state()
|
|
|
|
assert invalidations == [("k1-a", "test-session-k1-a")]
|
|
assert state["device_session"] is None
|
|
assert state["selected_device_id"] is None
|
|
assert state["k1_ip"] is None
|
|
assert service._ble_discovery_generation == discovery_generation # noqa: SLF001
|
|
assert [item["device_id"] for item in state["devices"]] == ["k1-a"]
|
|
assert state["connection_policy"]["actions"]["provision-fresh-device"]["allowed"] is True
|
|
assert "свежий Bluetooth-кандидат можно настроить заново" in state["message"]
|
|
|
|
|
|
def test_three_proven_host_path_losses_retire_session_and_require_new_scan(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_supervised_connection(
|
|
service,
|
|
target_ipv4="192.168.68.50",
|
|
connection_mode="bridge",
|
|
transport_ref="k1-a",
|
|
logical_device_id="device-a",
|
|
)
|
|
invalidations: list[bool] = []
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"invalidate_connected_device_session",
|
|
lambda *_args, **_kwargs: invalidations.append(True) or True,
|
|
)
|
|
lost_path = HostPathProbeResult(
|
|
available=False,
|
|
fingerprint=None,
|
|
interface=None,
|
|
source_ipv4=None,
|
|
route_class="unavailable",
|
|
reason_code="router-link-lost",
|
|
)
|
|
|
|
service._connection_supervisor.observe_host_path(lost_path) # noqa: SLF001
|
|
first = service.state()
|
|
assert first["device_session"] is not None
|
|
assert first["connection_mode"] == "bridge"
|
|
assert invalidations == []
|
|
|
|
# Unrelated supervisor revisions must not count as additional host-path
|
|
# failures while the same negative observation remains current.
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
current_intent = supervisor.snapshot().intent
|
|
assert current_intent is not None
|
|
for _ in range(2):
|
|
supervisor.set_intent(
|
|
intent_id=current_intent.intent_id,
|
|
requested_mode=current_intent.requested_mode,
|
|
expected_device_id=current_intent.expected_device_id,
|
|
)
|
|
unchanged = service.state()
|
|
assert unchanged["device_session"] is not None
|
|
assert invalidations == []
|
|
|
|
service._connection_supervisor.observe_host_path(lost_path) # noqa: SLF001
|
|
second = service.state()
|
|
assert second["device_session"] is not None
|
|
assert invalidations == []
|
|
|
|
service._connection_supervisor.observe_host_path(lost_path) # noqa: SLF001
|
|
third = service.state()
|
|
assert invalidations == [True]
|
|
assert third["device_session"] is None
|
|
assert third["connection_mode"] is None
|
|
assert third["k1_ip"] is None
|
|
assert service._selected_device_id is None # noqa: SLF001
|
|
assert service._connection_monitor_target() is None # noqa: SLF001
|
|
assert third["connection_policy"]["actions"]["provision-fresh-device"]["allowed"] is False
|
|
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
fresh = service.state()
|
|
assert fresh["connection_policy"]["actions"]["provision-fresh-device"]["allowed"] is True
|
|
|
|
|
|
@pytest.mark.parametrize("plane", ["host", "endpoint"])
|
|
def test_positive_transport_edge_resets_loss_streak_between_state_polls(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
plane: str,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_supervised_connection(
|
|
service,
|
|
target_ipv4="192.168.68.50",
|
|
connection_mode="bridge",
|
|
transport_ref="k1-a",
|
|
logical_device_id="device-a",
|
|
with_control=False,
|
|
)
|
|
invalidations: list[bool] = []
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"invalidate_connected_device_session",
|
|
lambda *_args, **_kwargs: invalidations.append(True) or True,
|
|
)
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
|
|
def observe(reachable: bool) -> None:
|
|
snapshot = supervisor.snapshot()
|
|
assert snapshot.intent is not None
|
|
assert snapshot.device_network.target is not None
|
|
if plane == "host":
|
|
supervisor.observe_host_path(
|
|
_direct_host_path("192.168.68.50")
|
|
if reachable
|
|
else HostPathProbeResult(
|
|
available=False,
|
|
fingerprint=None,
|
|
interface=None,
|
|
source_ipv4=None,
|
|
route_class="unavailable",
|
|
reason_code="router-link-lost",
|
|
)
|
|
)
|
|
else:
|
|
accepted = supervisor.observe_endpoint(
|
|
target=snapshot.device_network.target,
|
|
intent_id=snapshot.intent.intent_id,
|
|
host_path_epoch=snapshot.host_path.epoch,
|
|
reachable=reachable,
|
|
reason_code=None if reachable else "k1-control-port-unreachable",
|
|
)
|
|
assert accepted is True
|
|
|
|
observe(False)
|
|
assert service.state()["device_session"] is not None
|
|
observe(True) # The UI does not poll state() on this recovery edge.
|
|
observe(False)
|
|
assert service.state()["device_session"] is not None
|
|
observe(False)
|
|
assert service.state()["device_session"] is not None
|
|
assert invalidations == []
|
|
observe(False)
|
|
assert service.state()["device_session"] is None
|
|
assert invalidations == [True]
|
|
|
|
|
|
@pytest.mark.parametrize("plane", ["host", "endpoint"])
|
|
def test_positive_edge_between_loss_confirmation_and_teardown_cancels_retirement(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
plane: str,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_supervised_connection(
|
|
service,
|
|
target_ipv4="192.168.68.50",
|
|
connection_mode="bridge",
|
|
transport_ref="k1-a",
|
|
logical_device_id="device-a",
|
|
with_control=False,
|
|
)
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
|
|
def observe(reachable: bool) -> None:
|
|
snapshot = supervisor.snapshot()
|
|
assert snapshot.intent is not None
|
|
assert snapshot.device_network.target is not None
|
|
if plane == "host":
|
|
supervisor.observe_host_path(
|
|
_direct_host_path("192.168.68.50")
|
|
if reachable
|
|
else HostPathProbeResult(
|
|
available=False,
|
|
fingerprint=None,
|
|
interface=None,
|
|
source_ipv4=None,
|
|
route_class="unavailable",
|
|
reason_code="router-link-lost",
|
|
)
|
|
)
|
|
else:
|
|
accepted = supervisor.observe_endpoint(
|
|
target=snapshot.device_network.target,
|
|
intent_id=snapshot.intent.intent_id,
|
|
host_path_epoch=snapshot.host_path.epoch,
|
|
reachable=reachable,
|
|
reason_code=None if reachable else "k1-control-port-unreachable",
|
|
)
|
|
assert accepted is True
|
|
|
|
for _ in range(3):
|
|
observe(False)
|
|
|
|
original_snapshot = supervisor.snapshot
|
|
snapshot_calls = 0
|
|
|
|
def snapshot_with_recovery_edge() -> Any:
|
|
nonlocal snapshot_calls
|
|
snapshot_calls += 1
|
|
if snapshot_calls == 2:
|
|
before_recovery = original_snapshot()
|
|
assert before_recovery.intent is not None
|
|
assert before_recovery.device_network.target is not None
|
|
if plane == "host":
|
|
supervisor.observe_host_path(_direct_host_path("192.168.68.50"))
|
|
else:
|
|
accepted = supervisor.observe_endpoint(
|
|
target=before_recovery.device_network.target,
|
|
intent_id=before_recovery.intent.intent_id,
|
|
host_path_epoch=before_recovery.host_path.epoch,
|
|
reachable=True,
|
|
)
|
|
assert accepted is True
|
|
return original_snapshot()
|
|
|
|
monkeypatch.setattr(supervisor, "snapshot", snapshot_with_recovery_edge)
|
|
invalidations: list[bool] = []
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"invalidate_connected_device_session",
|
|
lambda *_args, **_kwargs: invalidations.append(True) or True,
|
|
)
|
|
|
|
service._retire_ephemeral_connection_binding_on_proven_loss( # noqa: SLF001
|
|
service._application_control_session.snapshot() # noqa: SLF001
|
|
)
|
|
|
|
assert snapshot_calls == 2
|
|
assert service._device_session_id is not None # noqa: SLF001
|
|
assert service._selected_device_id == "k1-a" # noqa: SLF001
|
|
assert invalidations == []
|
|
|
|
|
|
def test_old_loss_teardown_preserves_candidate_from_new_scan_generation(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_supervised_connection(
|
|
service,
|
|
target_ipv4="192.168.68.50",
|
|
connection_mode="bridge",
|
|
transport_ref="k1-a",
|
|
logical_device_id="device-a",
|
|
)
|
|
service._device_session_discovery_generation = 4 # noqa: SLF001
|
|
service._ble_discovery_generation = 4 # noqa: SLF001
|
|
invalidations: list[tuple[str, str]] = []
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"invalidate_connected_device_session",
|
|
lambda device_id, *, device_session_id: (
|
|
invalidations.append((device_id, device_session_id)) or True
|
|
),
|
|
)
|
|
lost_path = HostPathProbeResult(
|
|
available=False,
|
|
fingerprint=None,
|
|
interface=None,
|
|
source_ipv4=None,
|
|
route_class="unavailable",
|
|
reason_code="router-link-lost",
|
|
)
|
|
|
|
service._connection_supervisor.observe_host_path(lost_path) # noqa: SLF001
|
|
service.state()
|
|
# A new explicit scan generation completes while old-loss confirmation is
|
|
# still pending. Its selected candidate belongs to the next operator
|
|
# intent and must not be erased by teardown of the old device-session.
|
|
service._ble_discovery_generation = 5 # noqa: SLF001
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
|
|
service._connection_supervisor.observe_host_path(lost_path) # noqa: SLF001
|
|
service.state()
|
|
service._connection_supervisor.observe_host_path(lost_path) # noqa: SLF001
|
|
state = service.state()
|
|
|
|
assert invalidations == [("k1-a", "test-session-k1-a")]
|
|
assert state["device_session"] is None
|
|
assert [item["device_id"] for item in state["devices"]] == ["k1-a"]
|
|
assert state["connection_policy"]["actions"]["provision-fresh-device"]["allowed"] is True
|
|
|
|
|
|
def test_connection_mode_and_discovery_cas_reject_stale_connect_before_io(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
initial = service.state()
|
|
assert initial["desired_connection_mode"] == "bridge"
|
|
assert initial["desired_connection_mode_revision"] == 0
|
|
|
|
selected = service.select_connection_mode(
|
|
DesiredConnectionModeRequest(
|
|
connection_mode="quick-connect",
|
|
expected_revision=0,
|
|
)
|
|
)
|
|
assert selected["desired_connection_mode"] == "quick-connect"
|
|
assert selected["desired_connection_mode_revision"] == 1
|
|
assert selected["configured_connection_mode"] is None
|
|
assert selected["active_connection_mode"] is None
|
|
assert selected["ble_discovery_generation"] == 0
|
|
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as stale_select:
|
|
service.select_connection_mode(
|
|
DesiredConnectionModeRequest(
|
|
connection_mode="direct-connect",
|
|
expected_revision=0,
|
|
)
|
|
)
|
|
assert stale_select.value.reason_code == "connection-mode-draft-revision-conflict"
|
|
|
|
io_calls: list[str] = []
|
|
|
|
def forbidden_preflight(*_: object, **__: object) -> dict[str, Any]:
|
|
io_calls.append("host-preflight")
|
|
raise AssertionError("stale mode/generation must fail before host or device I/O")
|
|
|
|
async def forbidden_provision(*_: object, **__: object) -> dict[str, Any]:
|
|
io_calls.append("ble-write")
|
|
raise AssertionError("stale mode/generation must fail before BLE")
|
|
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"ensure_wifi_profile_from_credential_source",
|
|
forbidden_preflight,
|
|
)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_provision)
|
|
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as stale_mode:
|
|
asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
connection_mode="quick-connect",
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
expected_mode_revision=0,
|
|
)
|
|
)
|
|
)
|
|
assert stale_mode.value.reason_code == "connection-mode-draft-revision-conflict"
|
|
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as stale_scan:
|
|
asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
connection_mode="quick-connect",
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
expected_mode_revision=1,
|
|
expected_discovery_generation=1,
|
|
)
|
|
)
|
|
)
|
|
assert stale_scan.value.reason_code == "network-provision-discovery-generation-conflict"
|
|
assert io_calls == []
|
|
assert service.state()["operations"] == []
|
|
|
|
|
|
def test_pending_mode_scan_retires_prestart_control_without_physical_edge(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_select_connection_mode(service, "quick-connect")
|
|
binding = _seed_supervised_connection(
|
|
service,
|
|
target_ipv4=facade_module.AP_FALLBACK_IPV4,
|
|
connection_mode="quick-connect",
|
|
transport_ref="k1-a",
|
|
logical_device_id="device-a",
|
|
)
|
|
control = FakeInteractiveControlSession(initial_state="idle")
|
|
control.open(connection_binding=binding)
|
|
control.verified_control = _verified_control_for_binding(
|
|
binding,
|
|
logical_device_id="device-a",
|
|
)
|
|
close_calls: list[tuple[int | None, int | None]] = []
|
|
original_close_prestart = control.close_prestart
|
|
|
|
def counted_close_prestart(
|
|
_self: FakeInteractiveControlSession,
|
|
*,
|
|
expected_session_generation: int | None = None,
|
|
expected_state_revision: int | None = None,
|
|
) -> dict[str, object]:
|
|
close_calls.append((expected_session_generation, expected_state_revision))
|
|
return original_close_prestart(
|
|
expected_session_generation=expected_session_generation,
|
|
expected_state_revision=expected_state_revision,
|
|
)
|
|
|
|
control.close_prestart = MethodType(counted_close_prestart, control) # type: ignore[method-assign]
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
service._acquire_application_control_process_lease() # noqa: SLF001
|
|
ready = service.state()
|
|
assert ready["active_connection_mode"] == "quick-connect"
|
|
assert ready["connection_lifecycle"]["connection_ready"] is True
|
|
expected_close_checkpoint = (
|
|
ready["application_control_session"]["session_generation"],
|
|
ready["application_control_session"]["state_revision"],
|
|
)
|
|
|
|
switched = service.select_connection_mode(
|
|
DesiredConnectionModeRequest(
|
|
connection_mode="bridge",
|
|
expected_revision=ready["desired_connection_mode_revision"],
|
|
)
|
|
)
|
|
assert switched["connection_lifecycle"]["mode_change"] == {
|
|
"state": "switch-selected",
|
|
"from": "quick-connect",
|
|
"to": "bridge",
|
|
}
|
|
assert switched["connection_policy"]["actions"]["scan-ble"]["allowed"] is True
|
|
assert switched["connection_policy"]["actions"]["start-acquisition"]["allowed"] is False
|
|
assert (
|
|
"connection-mode-switch-pending"
|
|
in switched["connection_policy"]["actions"]["start-acquisition"]["reason_codes"]
|
|
)
|
|
|
|
device_edges: list[str] = []
|
|
|
|
async def fake_scan(
|
|
_duration_seconds: float,
|
|
*,
|
|
on_admitted: Callable[[], None] | None = None,
|
|
) -> dict[str, Any]:
|
|
assert control.snapshot()["state"] == "idle"
|
|
assert service._connection_supervisor.snapshot().authority.control_allowed is False # noqa: SLF001
|
|
assert service._selected_device_id is None # noqa: SLF001
|
|
assert on_admitted is not None
|
|
on_admitted()
|
|
return _ble_scan_result("bridge-candidate")
|
|
|
|
async def forbidden_device_write(*_: object, **__: object) -> dict[str, Any]:
|
|
device_edges.append("network-write")
|
|
raise AssertionError("mode selection/scan must not write device network state")
|
|
|
|
monkeypatch.setattr(facade_module, "scan", fake_scan)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_device_write)
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"prepare",
|
|
lambda *_args, **_kwargs: device_edges.append("physical-command"),
|
|
)
|
|
|
|
scanned = asyncio.run(service.scan_ble(1.0))
|
|
|
|
assert close_calls == [expected_close_checkpoint]
|
|
assert device_edges == []
|
|
assert scanned["desired_connection_mode"] == "bridge"
|
|
assert scanned["configured_connection_mode"] is None
|
|
assert scanned["active_connection_mode"] is None
|
|
assert scanned["connection_lifecycle"]["connection_ready"] is False
|
|
assert [item["device_id"] for item in scanned["devices"]] == ["bridge-candidate"]
|
|
repeated = service.state()
|
|
assert repeated["active_connection_mode"] is None
|
|
assert repeated["selected_device_id"] is None
|
|
|
|
|
|
def test_pending_reopened_physical_state_blocks_connect_and_mode_scan_before_device_io(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
device_edges: list[str] = []
|
|
|
|
async def forbidden_device_write(*_: object, **__: object) -> dict[str, Any]:
|
|
device_edges.append("network-write")
|
|
raise AssertionError("pending physical reopen must fail before GATT/Wi-Fi")
|
|
|
|
async def forbidden_scan(*_: object, **__: object) -> dict[str, Any]:
|
|
device_edges.append("ble-scan")
|
|
raise AssertionError("unsafe mode transition must fail before BLE scan")
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_device_write)
|
|
monkeypatch.setattr(facade_module, "scan", forbidden_scan)
|
|
|
|
connect_service, _ = service_with_fake_runtime(tmp_path / "connect")
|
|
_set_scanned_k1(connect_service, device_id="k1-a")
|
|
pending_bridge = _pending_reopened_physical_snapshot()
|
|
monkeypatch.setattr(
|
|
connect_service._physical_command_coordinator, # noqa: SLF001
|
|
"snapshot",
|
|
lambda: pending_bridge,
|
|
)
|
|
connect_topology_before = (
|
|
connect_service._selected_device_id, # noqa: SLF001
|
|
connect_service._k1_ip, # noqa: SLF001
|
|
connect_service._connection_mode, # noqa: SLF001
|
|
connect_service._connection_supervisor.snapshot(), # noqa: SLF001
|
|
)
|
|
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as blocked_connect:
|
|
asyncio.run(
|
|
connect_service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="synthetic-network",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
assert blocked_connect.value.reason_code == "physical-command-reconciliation-required"
|
|
assert (
|
|
connect_service._selected_device_id, # noqa: SLF001
|
|
connect_service._k1_ip, # noqa: SLF001
|
|
connect_service._connection_mode, # noqa: SLF001
|
|
connect_service._connection_supervisor.snapshot(), # noqa: SLF001
|
|
) == connect_topology_before
|
|
|
|
mode_service, _ = service_with_fake_runtime(tmp_path / "mode")
|
|
_seed_supervised_connection(
|
|
mode_service,
|
|
transport_ref="k1-a",
|
|
connection_mode="bridge",
|
|
)
|
|
pending_mode = _pending_reopened_physical_snapshot()
|
|
monkeypatch.setattr(
|
|
mode_service._physical_command_coordinator, # noqa: SLF001
|
|
"snapshot",
|
|
lambda: pending_mode,
|
|
)
|
|
with mode_service._lock: # noqa: SLF001
|
|
mode_service._desired_connection_mode = "quick-connect" # noqa: SLF001
|
|
mode_service._desired_connection_mode_revision += 1 # noqa: SLF001
|
|
mode_topology_before = (
|
|
mode_service._selected_device_id, # noqa: SLF001
|
|
mode_service._k1_ip, # noqa: SLF001
|
|
mode_service._connection_mode, # noqa: SLF001
|
|
mode_service._connection_supervisor.snapshot(), # noqa: SLF001
|
|
)
|
|
|
|
with pytest.raises(facade_module.BleDiscoveryUnavailable) as blocked_scan:
|
|
asyncio.run(mode_service.scan_ble(1.0))
|
|
assert blocked_scan.value.reason_code == "connection-mode-switch-physical-state-unsafe"
|
|
assert (
|
|
mode_service._selected_device_id, # noqa: SLF001
|
|
mode_service._k1_ip, # noqa: SLF001
|
|
mode_service._connection_mode, # noqa: SLF001
|
|
mode_service._connection_supervisor.snapshot(), # noqa: SLF001
|
|
) == mode_topology_before
|
|
assert device_edges == []
|
|
|
|
|
|
def test_state_polling_auto_retires_terminal_prestart_control_once(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
|
|
class FailedPrestartControl:
|
|
def __init__(self) -> None:
|
|
self.retire_calls = 0
|
|
self.retired = False
|
|
|
|
def snapshot(self) -> dict[str, object]:
|
|
if self.retired:
|
|
return {"state": "idle", "can_open": True, "failure": None}
|
|
return {
|
|
"state": "failed",
|
|
"can_open": True,
|
|
"failure": {
|
|
"reason_code": "mqtt-device-info-timeout",
|
|
"modeling_command_attempted": False,
|
|
"safe_to_retry": True,
|
|
},
|
|
}
|
|
|
|
def retire_for_network_change(self, **_: object) -> dict[str, object]:
|
|
self.retire_calls += 1
|
|
self.retired = True
|
|
return self.snapshot()
|
|
|
|
control = FailedPrestartControl()
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
service._acquire_application_control_process_lease() # noqa: SLF001
|
|
|
|
first = service.state()
|
|
second = service.state()
|
|
|
|
assert control.retire_calls == 1
|
|
assert first["application_control_session"]["state"] == "idle"
|
|
assert second["application_control_session"]["state"] == "idle"
|
|
assert first["connection_policy"]["actions"]["scan-ble"]["allowed"] is True
|
|
assert service._application_control_process_lease_holders == set() # noqa: SLF001
|
|
|
|
|
|
def _install_authoritative_ready_bridge_control(
|
|
service: XgridsK1CompatibilityService,
|
|
*,
|
|
transport_ref: str = "k1-a",
|
|
) -> tuple[FakeInteractiveControlSession, ApplicationConnectionBinding, dict[str, Any]]:
|
|
binding = _seed_supervised_connection(
|
|
service,
|
|
connection_mode="bridge",
|
|
transport_ref=transport_ref,
|
|
logical_device_id="device-a",
|
|
)
|
|
control = FakeInteractiveControlSession(initial_state="idle")
|
|
control.open(connection_binding=binding)
|
|
control.verified_control = _verified_control_for_binding(
|
|
binding,
|
|
logical_device_id="device-a",
|
|
control_session_id=f"test-control-{transport_ref}",
|
|
control_proof_revision=2,
|
|
)
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
service._reconcile_connection_supervisor( # noqa: SLF001
|
|
control.snapshot(),
|
|
service.runtime.snapshot(),
|
|
)
|
|
service._acquire_application_control_process_lease() # noqa: SLF001
|
|
state = service.state()
|
|
assert state["active_connection_mode"] == "bridge"
|
|
assert state["connection_lifecycle"]["active_binding_key"] is not None
|
|
return control, binding, state
|
|
|
|
|
|
def _reconfiguration_request(
|
|
state: dict[str, Any],
|
|
intent: str,
|
|
) -> PrepareConnectionReconfigurationRequest:
|
|
reconfiguration = state["connection_reconfiguration"]
|
|
lifecycle = state["connection_lifecycle"]
|
|
return PrepareConnectionReconfigurationRequest(
|
|
intent=intent, # type: ignore[arg-type]
|
|
expected_reconfiguration_revision=reconfiguration["revision"],
|
|
expected_reconfiguration_intent_id=reconfiguration["intent_id"],
|
|
expected_desired_mode_revision=state["desired_connection_mode_revision"],
|
|
expected_active_binding_key=lifecycle["active_binding_key"],
|
|
)
|
|
|
|
|
|
def _seed_durable_bridge_topology(
|
|
service: XgridsK1CompatibilityService,
|
|
*,
|
|
transport_ref: str,
|
|
ipv4: str,
|
|
) -> None:
|
|
store = service._semantic_topology_store # noqa: SLF001
|
|
assert store is not None
|
|
snapshot = store.snapshot()
|
|
store.commit(
|
|
transport_ref=transport_ref,
|
|
connection_mode="bridge",
|
|
ipv4=ipv4,
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
firmware_version="3.0.2",
|
|
source="ble-read-only-status",
|
|
observed_at_utc="2026-08-10T12:00:00Z",
|
|
predecessor_revision=(snapshot.record.revision if snapshot.record is not None else 0),
|
|
)
|
|
|
|
|
|
def _install_successful_bridge_verify_transport(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
*,
|
|
device_id: str,
|
|
ipv4: str,
|
|
writes: list[str],
|
|
) -> None:
|
|
async def existing_network_status(*_: object, **__: object) -> dict[str, Any]:
|
|
return {
|
|
**_wifi_status_read(ipv4),
|
|
"device_macos_uuid": device_id,
|
|
}
|
|
|
|
async def forbidden_network_write(*_: object, **__: object) -> dict[str, Any]:
|
|
writes.append("network-write")
|
|
raise AssertionError("read-only Verify must not provision Wi-Fi")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", existing_network_status)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_network_write)
|
|
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)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
|
|
|
|
def test_read_only_monitor_holder_does_not_flap_reconfiguration_policy(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_control, _binding, ready = _install_authoritative_ready_bridge_control(service)
|
|
ready_binding_key = ready["connection_lifecycle"]["active_binding_key"]
|
|
ready_host_epoch = ready["connection_supervisor"]["observed"]["host_path"]["epoch"]
|
|
|
|
assert ready["connection_policy"]["actions"]["prepare-select-device"]["allowed"] is True
|
|
assert ready["connection_policy"]["actions"]["prepare-change-network"]["allowed"] is True
|
|
|
|
service._acquire_k1_lifecycle_process_lease("monitor") # noqa: SLF001
|
|
try:
|
|
during_probe = service.state()
|
|
finally:
|
|
service._release_k1_lifecycle_process_lease("monitor") # noqa: SLF001
|
|
|
|
assert "monitor" in during_probe["k1_lifecycle_process_lease"]["holders"]
|
|
assert during_probe["connection_lifecycle"]["active_binding_key"] == ready_binding_key
|
|
assert (
|
|
during_probe["connection_supervisor"]["observed"]["host_path"]["epoch"] == ready_host_epoch
|
|
)
|
|
for action in ("prepare-select-device", "prepare-change-network"):
|
|
decision = during_probe["connection_policy"]["actions"][action]
|
|
assert decision["allowed"] is True
|
|
assert "k1-lifecycle-process-lease-network-owned" not in decision["reason_codes"]
|
|
|
|
|
|
def test_monitor_transient_wifi_observer_failures_retain_verified_control_and_refresh_tcp(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_control, binding, _ready = _install_authoritative_ready_bridge_control(service)
|
|
initial = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
assert initial.authority.control_allowed is True
|
|
tcp_calls: list[str] = []
|
|
|
|
def association_observation(
|
|
reason_code: str,
|
|
token: str,
|
|
) -> HostWifiAssociationIdentityResult:
|
|
return {
|
|
"schema_version": 1,
|
|
"adapter": "unavailable",
|
|
"wifi_interface": None,
|
|
"association_state": "unavailable",
|
|
"evidence_quality": "unavailable",
|
|
"continuity_proven": False,
|
|
"continuity_token": token,
|
|
"reason_code": reason_code,
|
|
}
|
|
|
|
association_calls = 0
|
|
|
|
def alternating_unproven_association(
|
|
_interface_name: str | None,
|
|
*,
|
|
timeout_seconds: float = 30.0,
|
|
) -> HostWifiAssociationIdentityResult:
|
|
nonlocal association_calls
|
|
assert timeout_seconds == (facade_module.CONNECTION_MONITOR_ASSOCIATION_TIMEOUT_SECONDS)
|
|
association_calls += 1
|
|
return (
|
|
association_observation("host-wifi-operation-timeout", "a" * 64)
|
|
if association_calls % 2
|
|
else association_observation("association-identity-unavailable", "b" * 64)
|
|
)
|
|
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(
|
|
service._host_wifi_association_probe, # noqa: SLF001
|
|
"observe",
|
|
alternating_unproven_association,
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_probe_control_endpoint_socket",
|
|
lambda target: (
|
|
tcp_calls.append(target) or facade_module.TcpReachabilityProbeResult(reachable=True)
|
|
),
|
|
)
|
|
|
|
async def three_mixed_cycles() -> tuple[
|
|
facade_module.ConnectionSupervisorSnapshot,
|
|
facade_module.ConnectionSupervisorSnapshot,
|
|
facade_module.ConnectionSupervisorSnapshot,
|
|
]:
|
|
first = await service._connection_monitor.poll_once() # noqa: SLF001
|
|
second = await service._connection_monitor.poll_once() # noqa: SLF001
|
|
third = await service._connection_monitor.poll_once() # noqa: SLF001
|
|
return first, second, third
|
|
|
|
retained_cycles = asyncio.run(three_mixed_cycles())
|
|
|
|
assert association_calls == 6
|
|
assert tcp_calls == [binding.target_ipv4] * 3
|
|
for retained in retained_cycles:
|
|
assert retained.revision > initial.revision
|
|
assert retained.host_path.available is True
|
|
assert retained.host_path.epoch == initial.host_path.epoch
|
|
assert retained.host_path.fingerprint == initial.host_path.fingerprint
|
|
assert retained.endpoint.tcp_state == "reachable"
|
|
assert retained.endpoint.host_path_epoch == initial.host_path.epoch
|
|
assert retained.device_identity.state == "verified"
|
|
assert retained.control_plane.state == "healthy"
|
|
assert retained.authority.control_allowed is True
|
|
assert retained.lease.state == "reachable"
|
|
assert retained.lease.generation == initial.lease.generation
|
|
assert service._connection_monitor_contact_gate.locked() is False # noqa: SLF001
|
|
assert service._application_control_process_lease_holders == {"control"} # noqa: SLF001
|
|
|
|
def privacy_limited_association(
|
|
_interface_name: str | None,
|
|
*,
|
|
timeout_seconds: float = 30.0,
|
|
) -> HostWifiAssociationIdentityResult:
|
|
assert timeout_seconds == (facade_module.CONNECTION_MONITOR_ASSOCIATION_TIMEOUT_SECONDS)
|
|
return association_observation("association-identity-unavailable", "b" * 64)
|
|
|
|
monkeypatch.setattr(
|
|
service._host_wifi_association_probe, # noqa: SLF001
|
|
"observe",
|
|
privacy_limited_association,
|
|
)
|
|
privacy_path = service._sample_host_path( # noqa: SLF001
|
|
binding.target_ipv4,
|
|
association_timeout_seconds=(facade_module.CONNECTION_MONITOR_ASSOCIATION_TIMEOUT_SECONDS),
|
|
fail_closed_unproven_association=True,
|
|
)
|
|
|
|
assert privacy_path.available is True
|
|
assert privacy_path.reason_code is None
|
|
assert privacy_path.observation_failure_class == "association-observer"
|
|
assert privacy_path.fingerprint == initial.host_path.fingerprint
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("reason_code", "expected_failure_class"),
|
|
[
|
|
("host-wifi-operation-timeout", "association-observer"),
|
|
("wifi-interface-inactive", "route"),
|
|
],
|
|
)
|
|
def test_monitor_debounces_only_technical_association_timeout(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
reason_code: str,
|
|
expected_failure_class: str,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
|
|
def unavailable_association(
|
|
_interface_name: str | None,
|
|
*,
|
|
timeout_seconds: float = 30.0,
|
|
) -> HostWifiAssociationIdentityResult:
|
|
assert timeout_seconds == 3.0
|
|
return {
|
|
"schema_version": 1,
|
|
"adapter": "CoreWLAN",
|
|
"wifi_interface": True,
|
|
"association_state": "unavailable",
|
|
"evidence_quality": "unavailable",
|
|
"continuity_proven": False,
|
|
"continuity_token": "a" * 64,
|
|
"reason_code": reason_code,
|
|
}
|
|
|
|
monkeypatch.setattr(
|
|
service._host_wifi_association_probe, # noqa: SLF001
|
|
"observe",
|
|
unavailable_association,
|
|
)
|
|
|
|
path = service._sample_host_path( # noqa: SLF001
|
|
"192.168.68.51",
|
|
association_timeout_seconds=3.0,
|
|
fail_closed_unproven_association=True,
|
|
)
|
|
|
|
assert path.available is False
|
|
assert path.reason_code == reason_code
|
|
assert path.observation_failure_class == expected_failure_class
|
|
assert path.kernel_route_fingerprint == "test-route:192.168.68.51"
|
|
|
|
|
|
def test_monitor_association_sample_over_two_seconds_survives_four_full_cycles(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_control, binding, _ready = _install_authoritative_ready_bridge_control(service)
|
|
host_samples: list[str] = []
|
|
tcp_samples: list[str] = []
|
|
|
|
def slow_first_host_sample(
|
|
target: str,
|
|
*,
|
|
association_timeout_seconds: float = 30.0,
|
|
fail_closed_unproven_association: bool = False,
|
|
) -> HostPathProbeResult:
|
|
assert association_timeout_seconds == 3.0
|
|
assert association_timeout_seconds == (
|
|
facade_module.CONNECTION_MONITOR_ASSOCIATION_TIMEOUT_SECONDS
|
|
)
|
|
assert fail_closed_unproven_association is True
|
|
host_samples.append(target)
|
|
if len(host_samples) % 2 == 1:
|
|
# Reproduce the field boundary that exceeded the former 2 s
|
|
# xcrun/CoreWLAN deadline without making the test depend on macOS.
|
|
time.sleep(2.05)
|
|
return _direct_host_path(target)
|
|
|
|
monkeypatch.setattr(service, "_sample_host_path", slow_first_host_sample)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_probe_control_endpoint_socket",
|
|
lambda target: (
|
|
tcp_samples.append(target) or facade_module.TcpReachabilityProbeResult(reachable=True)
|
|
),
|
|
)
|
|
|
|
async def four_cycles() -> tuple[list[Any], list[float], list[dict[str, Any]]]:
|
|
snapshots: list[Any] = []
|
|
elapsed_cycles: list[float] = []
|
|
public_states: list[dict[str, Any]] = []
|
|
for _ in range(4):
|
|
started = time.monotonic()
|
|
snapshots.append(
|
|
await service._connection_monitor.poll_once() # noqa: SLF001
|
|
)
|
|
elapsed_cycles.append(time.monotonic() - started)
|
|
public_states.append(service.state())
|
|
return snapshots, elapsed_cycles, public_states
|
|
|
|
refreshed_cycles, elapsed_cycles, public_states = asyncio.run(four_cycles())
|
|
|
|
assert all(
|
|
2.0 < elapsed < DEFAULT_TRANSPORT_OBSERVATION_TTL_SECONDS for elapsed in elapsed_cycles
|
|
)
|
|
assert facade_module.CONNECTION_MONITOR_HOST_CONTACT_BOUND_SECONDS == 5.0
|
|
assert (
|
|
facade_module.CONNECTION_MONITOR_HOST_CONTACT_BOUND_SECONDS
|
|
< facade_module.CONNECTION_MONITOR_QUIESCE_TIMEOUT_SECONDS
|
|
)
|
|
assert facade_module.CONNECTION_MONITOR_FULL_PASS_BOUND_SECONDS == 11.5
|
|
assert (
|
|
2 * (facade_module.CONNECTION_MONITOR_FULL_PASS_BOUND_SECONDS + 1.0)
|
|
< DEFAULT_TRANSPORT_OBSERVATION_TTL_SECONDS
|
|
)
|
|
assert host_samples == [binding.target_ipv4, binding.target_ipv4] * 4
|
|
assert tcp_samples == [binding.target_ipv4] * 4
|
|
initial_epoch = refreshed_cycles[0].host_path.epoch
|
|
initial_lease_generation = refreshed_cycles[0].lease.generation
|
|
initial_selected = public_states[0]["selected_device_id"]
|
|
initial_session = public_states[0]["device_session"]["device_session_id"]
|
|
for refreshed, public in zip(refreshed_cycles, public_states, strict=True):
|
|
assert refreshed.host_path.available is True
|
|
assert refreshed.host_path.epoch == initial_epoch
|
|
assert refreshed.endpoint.tcp_state == "reachable"
|
|
assert refreshed.lease.state == "reachable"
|
|
assert refreshed.lease.generation == initial_lease_generation
|
|
assert refreshed.authority.control_allowed is True
|
|
assert public["selected_device_id"] == initial_selected
|
|
assert public["device_session"]["device_session_id"] == initial_session
|
|
assert public["application_control_session"]["state"] == "connection-ready"
|
|
|
|
|
|
def test_monitor_refreshes_verified_binding_across_timeout_positive_timeout_cycles(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
control, binding, ready = _install_authoritative_ready_bridge_control(service)
|
|
baseline_supervisor = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
baseline_path = baseline_supervisor.host_path
|
|
assert baseline_path.kernel_route_fingerprint is not None
|
|
selected_device_id = ready["selected_device_id"]
|
|
device_session_id = ready["device_session"]["device_session_id"]
|
|
active_binding_key = ready["connection_lifecycle"]["active_binding_key"]
|
|
tcp_samples: list[str] = []
|
|
probe_mode = {"technical_failure": True}
|
|
|
|
def sample_host(target: str, **_: object) -> HostPathProbeResult:
|
|
if not probe_mode["technical_failure"]:
|
|
return _direct_host_path(target)
|
|
return HostPathProbeResult(
|
|
available=False,
|
|
fingerprint=None,
|
|
interface=baseline_path.interface,
|
|
source_ipv4=baseline_path.source_ipv4,
|
|
route_class="unavailable",
|
|
reason_code="host-wifi-operation-timeout",
|
|
observation_failure_class="association-observer",
|
|
kernel_route_fingerprint=baseline_path.kernel_route_fingerprint,
|
|
)
|
|
|
|
monkeypatch.setattr(service, "_sample_host_path", sample_host)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_probe_control_endpoint_socket",
|
|
lambda target: (
|
|
tcp_samples.append(target) or facade_module.TcpReachabilityProbeResult(reachable=True)
|
|
),
|
|
)
|
|
|
|
async def scenario() -> tuple[list[Any], list[dict[str, Any]]]:
|
|
snapshots: list[Any] = []
|
|
states: list[dict[str, Any]] = []
|
|
for _ in range(2):
|
|
snapshots.append(
|
|
await service._connection_monitor.poll_once() # noqa: SLF001
|
|
)
|
|
states.append(service.state())
|
|
probe_mode["technical_failure"] = False
|
|
snapshots.append(await service._connection_monitor.poll_once()) # noqa: SLF001
|
|
states.append(service.state())
|
|
probe_mode["technical_failure"] = True
|
|
for _ in range(5):
|
|
snapshots.append(
|
|
await service._connection_monitor.poll_once() # noqa: SLF001
|
|
)
|
|
states.append(service.state())
|
|
return snapshots, states
|
|
|
|
monitor_states, public_states = asyncio.run(scenario())
|
|
assert len(monitor_states) == 8
|
|
assert tcp_samples == [binding.target_ipv4] * 8
|
|
previous_revision = baseline_supervisor.revision
|
|
for retained, public in zip(monitor_states, public_states, strict=True):
|
|
assert retained.revision > previous_revision
|
|
previous_revision = retained.revision
|
|
assert retained.host_path.available is True
|
|
assert retained.host_path.epoch == baseline_path.epoch
|
|
assert retained.host_path.fingerprint == baseline_path.fingerprint
|
|
assert retained.host_path.reason_code is None
|
|
assert retained.host_path_negative_streak == 0
|
|
assert retained.endpoint.tcp_state == "reachable"
|
|
assert retained.endpoint.host_path_epoch == baseline_path.epoch
|
|
assert retained.device_identity.state == "verified"
|
|
assert retained.control_plane.state == "healthy"
|
|
assert retained.lease.state == "reachable"
|
|
assert retained.lease.generation == baseline_supervisor.lease.generation
|
|
assert retained.authority.control_allowed is True
|
|
assert public["selected_device_id"] == selected_device_id
|
|
assert public["device_session"]["device_session_id"] == device_session_id
|
|
assert public["connection_lifecycle"]["active_binding_key"] == active_binding_key
|
|
assert public["application_control_session"]["state"] == "connection-ready"
|
|
assert public["k1_lifecycle_process_lease"]["holders"] == ["control"]
|
|
assert monitor_states[-1].host_path.observed_at != baseline_path.observed_at
|
|
assert control.state == "connection-ready"
|
|
|
|
|
|
def test_command_preflight_retains_exact_route_when_only_association_observer_times_out(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_control, binding, _ready = _install_authoritative_ready_bridge_control(service)
|
|
baseline = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
association_calls = 0
|
|
|
|
def timed_out_association(
|
|
_interface_name: str | None,
|
|
*,
|
|
timeout_seconds: float = 30.0,
|
|
) -> HostWifiAssociationIdentityResult:
|
|
nonlocal association_calls
|
|
assert timeout_seconds == facade_module.COMMAND_BOUND_ASSOCIATION_TIMEOUT_SECONDS
|
|
association_calls += 1
|
|
return {
|
|
"schema_version": 1,
|
|
"adapter": "CoreWLAN",
|
|
"wifi_interface": True,
|
|
"association_state": "unavailable",
|
|
"evidence_quality": "unavailable",
|
|
"continuity_proven": False,
|
|
"continuity_token": f"{association_calls:064x}",
|
|
"reason_code": "host-wifi-operation-timeout",
|
|
}
|
|
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(
|
|
service._host_wifi_association_probe, # noqa: SLF001
|
|
"observe",
|
|
timed_out_association,
|
|
)
|
|
|
|
for _ in range(5):
|
|
service._validate_application_connection_path(binding) # noqa: SLF001
|
|
retained = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
assert retained.host_path.available is True
|
|
assert retained.host_path.epoch == baseline.host_path.epoch
|
|
assert retained.host_path.fingerprint == baseline.host_path.fingerprint
|
|
assert retained.endpoint.tcp_state == "reachable"
|
|
assert retained.device_identity.state == "verified"
|
|
assert retained.control_plane.state == "healthy"
|
|
assert retained.lease.state == "reachable"
|
|
assert retained.authority.control_allowed is True
|
|
|
|
assert association_calls == 5
|
|
|
|
|
|
def test_command_prepare_allows_contended_probe_plus_own_helper_within_eight_seconds(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
harness = DeterministicContendedAssociationProbe(
|
|
lock_wait_seconds=3.0,
|
|
helper_required_seconds=3.2,
|
|
)
|
|
probe = harness.build(monkeypatch, tmp_path)
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
service._host_wifi_association_probe = probe # type: ignore[assignment] # noqa: SLF001
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
supervisor._monotonic_clock = lambda: harness.monotonic # noqa: SLF001
|
|
supervisor._suspend_aware_clock = lambda: harness.wall # noqa: SLF001
|
|
# The helper crosses this deliberately narrow regression TTL. A fresh
|
|
# exact same route/association result is itself the replacement sample and
|
|
# must refresh the original epoch before any snapshot reduction runs.
|
|
supervisor._observation_ttl_seconds = 5.0 # noqa: SLF001
|
|
control, binding, _ready = _install_binding_validating_ready_control(
|
|
service,
|
|
host_path=_association_bound_direct_host_path("192.168.1.20"),
|
|
)
|
|
target = EndpointTarget(binding.target_ipv4, binding.target_port)
|
|
harness.on_first_lock_acquired = lambda: supervisor.observe_endpoint(
|
|
target=target,
|
|
intent_id=binding.intent_id,
|
|
host_path_epoch=binding.host_path_epoch,
|
|
reachable=True,
|
|
)
|
|
control.state = "workspace-ready"
|
|
control.state_revision += 1
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host=binding.target_ipv4,
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_control_session_generation=control.session_generation,
|
|
expected_control_state_revision=control.state_revision,
|
|
)
|
|
)
|
|
|
|
assert facade_module.CONNECTION_MONITOR_ASSOCIATION_TIMEOUT_SECONDS == 3.0
|
|
assert facade_module.COMMAND_BOUND_ASSOCIATION_TIMEOUT_SECONDS == 8.0
|
|
assert facade_module.CONNECTION_VERIFY_HARD_TIMEOUT_SECONDS == 125.0
|
|
assert prepared["acquisition"]["state"] == "prepared"
|
|
assert prepared["application_control_session"]["state"] == "project-ready"
|
|
assert supervisor.snapshot().host_path.epoch == binding.host_path_epoch
|
|
assert supervisor.snapshot().authority.control_allowed is True
|
|
assert control.validation_calls == 2
|
|
assert harness.lock_timeouts == [8.0, 8.0]
|
|
assert harness.helper_timeouts == [pytest.approx(5.0)]
|
|
assert harness.monotonic == pytest.approx(106.2)
|
|
|
|
|
|
def test_privacy_limited_monitor_keeps_prestart_authority_through_three_minutes(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
monotonic_now = [100.0]
|
|
suspend_aware_now = [1_000.0]
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
supervisor._monotonic_clock = lambda: monotonic_now[0] # noqa: SLF001
|
|
supervisor._suspend_aware_clock = lambda: suspend_aware_now[0] # noqa: SLF001
|
|
control, binding, _ready = _install_binding_validating_ready_control(
|
|
service,
|
|
host_path=_association_bound_direct_host_path("192.168.1.20"),
|
|
)
|
|
timeouts: list[float] = []
|
|
tcp_calls: list[str] = []
|
|
|
|
def privacy_limited_association(
|
|
_interface_name: str | None,
|
|
*,
|
|
timeout_seconds: float,
|
|
) -> HostWifiAssociationIdentityResult:
|
|
timeouts.append(timeout_seconds)
|
|
return {
|
|
"schema_version": 1,
|
|
"adapter": "CoreWLAN",
|
|
"wifi_interface": True,
|
|
"association_state": "unavailable",
|
|
"evidence_quality": "unavailable",
|
|
"continuity_proven": False,
|
|
"continuity_token": "d" * 64,
|
|
"reason_code": "association-identity-unavailable",
|
|
}
|
|
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(
|
|
service._host_wifi_association_probe, # noqa: SLF001
|
|
"observe",
|
|
privacy_limited_association,
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_probe_control_endpoint_socket",
|
|
lambda target: (
|
|
tcp_calls.append(target) or facade_module.TcpReachabilityProbeResult(reachable=True)
|
|
),
|
|
)
|
|
|
|
async def monitor_for_three_minutes() -> None:
|
|
for proof_revision in range(3, 33):
|
|
observed = await service._connection_monitor.poll_once() # noqa: SLF001
|
|
assert observed.host_path.epoch == binding.host_path_epoch
|
|
assert observed.authority.control_allowed is True
|
|
assert control.verified_control is not None
|
|
control.verified_control = {
|
|
**control.verified_control,
|
|
"control_proof_revision": proof_revision,
|
|
}
|
|
service._reconcile_connection_supervisor( # noqa: SLF001
|
|
control.snapshot(),
|
|
service.runtime.snapshot(),
|
|
)
|
|
monotonic_now[0] += 6.1
|
|
suspend_aware_now[0] += 6.1
|
|
|
|
asyncio.run(monitor_for_three_minutes())
|
|
assert monotonic_now[0] > 280.0
|
|
|
|
control.state = "workspace-ready"
|
|
control.state_revision += 1
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host=binding.target_ipv4,
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_control_session_generation=control.session_generation,
|
|
expected_control_state_revision=control.state_revision,
|
|
)
|
|
)
|
|
|
|
final = supervisor.snapshot()
|
|
assert prepared["acquisition"]["state"] == "prepared"
|
|
assert final.host_path.epoch == binding.host_path_epoch
|
|
assert final.host_path.fingerprint == (
|
|
_association_bound_direct_host_path(binding.target_ipv4).fingerprint
|
|
)
|
|
assert final.endpoint.tcp_state == "reachable"
|
|
assert final.device_identity.state == "verified"
|
|
assert final.control_plane.state == "healthy"
|
|
assert final.authority.control_allowed is True
|
|
assert tcp_calls == [binding.target_ipv4] * 30
|
|
assert timeouts.count(facade_module.CONNECTION_MONITOR_ASSOCIATION_TIMEOUT_SECONDS) == 60
|
|
assert timeouts.count(facade_module.COMMAND_BOUND_ASSOCIATION_TIMEOUT_SECONDS) == 2
|
|
|
|
|
|
def test_command_timeout_after_stale_epoch_fails_before_local_or_physical_mutation(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
|
|
def expire_current_epoch() -> None:
|
|
stale = supervisor.snapshot()
|
|
assert stale.host_path.reason_code == "host-path-observation-stale"
|
|
|
|
harness = DeterministicContendedAssociationProbe(
|
|
lock_wait_seconds=3.0,
|
|
helper_required_seconds=6.0,
|
|
on_first_lock_acquired=expire_current_epoch,
|
|
)
|
|
probe = harness.build(monkeypatch, tmp_path)
|
|
service._host_wifi_association_probe = probe # type: ignore[assignment] # noqa: SLF001
|
|
supervisor._monotonic_clock = lambda: harness.monotonic # noqa: SLF001
|
|
supervisor._suspend_aware_clock = lambda: harness.wall # noqa: SLF001
|
|
supervisor._observation_ttl_seconds = 2.0 # noqa: SLF001
|
|
control, binding, _ready = _install_binding_validating_ready_control(
|
|
service,
|
|
host_path=_association_bound_direct_host_path("192.168.1.20"),
|
|
)
|
|
control.state = "workspace-ready"
|
|
control.state_revision += 1
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
operations_before = service._operations.snapshot() # noqa: SLF001
|
|
physical_before = service._physical_command_ledger.snapshot() # noqa: SLF001
|
|
|
|
with pytest.raises(ApplicationConnectionBindingLost):
|
|
service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name=PROJECT_NAME,
|
|
host=binding.target_ipv4,
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_control_session_generation=control.session_generation,
|
|
expected_control_state_revision=control.state_revision,
|
|
)
|
|
)
|
|
|
|
stale = supervisor.snapshot()
|
|
assert harness.lock_timeouts == [8.0]
|
|
assert harness.helper_timeouts == [pytest.approx(5.0)]
|
|
assert harness.monotonic == pytest.approx(108.0)
|
|
assert stale.host_path.epoch == binding.host_path_epoch + 1
|
|
assert stale.host_path.reason_code == "host-path-observation-stale"
|
|
assert service._acquisition is None # noqa: SLF001
|
|
assert service._operations.snapshot() == operations_before # noqa: SLF001
|
|
assert service._physical_command_ledger.snapshot() == physical_before # noqa: SLF001
|
|
|
|
|
|
def test_command_privacy_bridge_never_retains_a_proven_association_change(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_control, binding, _ready = _install_binding_validating_ready_control(
|
|
service,
|
|
host_path=_association_bound_direct_host_path("192.168.1.20"),
|
|
)
|
|
baseline = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(
|
|
service._host_wifi_association_probe, # noqa: SLF001
|
|
"observe",
|
|
lambda *_args, **_kwargs: {
|
|
**DeterministicContendedAssociationProbe.proven_observation(),
|
|
"continuity_token": "b" * 64,
|
|
},
|
|
)
|
|
|
|
with pytest.raises(ApplicationConnectionBindingLost):
|
|
service._validate_application_connection_path(binding) # noqa: SLF001
|
|
|
|
changed = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
assert changed.host_path.epoch == baseline.host_path.epoch + 1
|
|
assert changed.host_path.kernel_route_fingerprint == (
|
|
baseline.host_path.kernel_route_fingerprint
|
|
)
|
|
assert changed.host_path.fingerprint != baseline.host_path.fingerprint
|
|
assert changed.authority.control_allowed is False
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"broken_fact",
|
|
[
|
|
"configured-unverified",
|
|
"identity-stale",
|
|
"identity-intent",
|
|
"identity-mode",
|
|
"identity-epoch",
|
|
"control-lost",
|
|
"control-session",
|
|
"control-epoch",
|
|
"lease-lost",
|
|
"lease-intent",
|
|
"lease-mode",
|
|
"lease-target",
|
|
"lease-epoch",
|
|
"authority-denied",
|
|
],
|
|
)
|
|
def test_command_privacy_bridge_requires_exact_verified_control_chain(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
broken_fact: str,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_control, binding, _ready = _install_binding_validating_ready_control(
|
|
service,
|
|
host_path=_association_bound_direct_host_path("192.168.1.20"),
|
|
)
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
baseline = supervisor.snapshot()
|
|
candidate = baseline
|
|
if broken_fact == "configured-unverified":
|
|
candidate = replace(
|
|
baseline,
|
|
device_identity=replace(baseline.device_identity, state="unverified"),
|
|
control_plane=replace(baseline.control_plane, state="idle", session_id=None),
|
|
lease=replace(baseline.lease, state="configured-unverified"),
|
|
authority=replace(
|
|
baseline.authority,
|
|
control_allowed=False,
|
|
acquisition_start_allowed=False,
|
|
),
|
|
)
|
|
elif broken_fact.startswith("identity-"):
|
|
identity_changes: dict[str, object] = {
|
|
"identity-stale": {"state": "stale"},
|
|
"identity-intent": {"intent_id": "other-intent"},
|
|
"identity-mode": {"connection_mode": "quick-connect"},
|
|
"identity-epoch": {"host_path_epoch": binding.host_path_epoch + 1},
|
|
}[broken_fact]
|
|
candidate = replace(
|
|
baseline,
|
|
device_identity=replace(baseline.device_identity, **identity_changes),
|
|
)
|
|
elif broken_fact.startswith("control-"):
|
|
control_changes: dict[str, object] = {
|
|
"control-lost": {"state": "lost"},
|
|
"control-session": {"session_id": " "},
|
|
"control-epoch": {"host_path_epoch": binding.host_path_epoch + 1},
|
|
}[broken_fact]
|
|
candidate = replace(
|
|
baseline,
|
|
control_plane=replace(baseline.control_plane, **control_changes),
|
|
)
|
|
elif broken_fact.startswith("lease-"):
|
|
lease_changes: dict[str, object] = {
|
|
"lease-lost": {"state": "lost"},
|
|
"lease-intent": {"intent_id": "other-intent"},
|
|
"lease-mode": {"connection_mode": "quick-connect"},
|
|
"lease-target": {
|
|
"target": EndpointTarget("192.168.1.21", binding.target_port)
|
|
},
|
|
"lease-epoch": {"host_path_epoch": binding.host_path_epoch + 1},
|
|
}[broken_fact]
|
|
candidate = replace(
|
|
baseline,
|
|
lease=replace(baseline.lease, **lease_changes),
|
|
)
|
|
else:
|
|
assert broken_fact == "authority-denied"
|
|
candidate = replace(
|
|
baseline,
|
|
authority=replace(
|
|
baseline.authority,
|
|
control_allowed=False,
|
|
acquisition_start_allowed=False,
|
|
),
|
|
)
|
|
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(
|
|
service._host_wifi_association_probe, # noqa: SLF001
|
|
"observe",
|
|
lambda *_args, **_kwargs: {
|
|
"schema_version": 1,
|
|
"adapter": "CoreWLAN",
|
|
"wifi_interface": True,
|
|
"association_state": "unavailable",
|
|
"evidence_quality": "unavailable",
|
|
"continuity_proven": False,
|
|
"continuity_token": "d" * 64,
|
|
"reason_code": "association-identity-unavailable",
|
|
},
|
|
)
|
|
monkeypatch.setattr(
|
|
supervisor,
|
|
"association_timeout_retention_candidate",
|
|
lambda *, expected_target: (
|
|
candidate
|
|
if expected_target == EndpointTarget(binding.target_ipv4, binding.target_port)
|
|
else None
|
|
),
|
|
)
|
|
|
|
sampled = service._sample_host_path(binding.target_ipv4) # noqa: SLF001
|
|
|
|
assert sampled.available is True
|
|
assert sampled.reason_code == "association-identity-unavailable"
|
|
assert sampled.fingerprint != baseline.host_path.fingerprint
|
|
|
|
|
|
@pytest.mark.parametrize("changed_raw_fact", ["kernel-route", "interface", "source-ipv4"])
|
|
def test_command_privacy_bridge_never_retains_a_raw_route_change(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
changed_raw_fact: str,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_control, binding, _ready = _install_binding_validating_ready_control(
|
|
service,
|
|
host_path=_association_bound_direct_host_path("192.168.1.20"),
|
|
)
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
baseline = supervisor.snapshot()
|
|
assert baseline.host_path.kernel_route_fingerprint is not None
|
|
changed_path = HostPathProbeResult(
|
|
available=True,
|
|
fingerprint=(
|
|
"changed-kernel-route"
|
|
if changed_raw_fact == "kernel-route"
|
|
else baseline.host_path.kernel_route_fingerprint
|
|
),
|
|
interface=(
|
|
"changed0" if changed_raw_fact == "interface" else baseline.host_path.interface
|
|
),
|
|
source_ipv4=(
|
|
"192.168.99.2"
|
|
if changed_raw_fact == "source-ipv4"
|
|
else baseline.host_path.source_ipv4
|
|
),
|
|
route_class="direct",
|
|
kernel_route_fingerprint=(
|
|
"changed-kernel-route"
|
|
if changed_raw_fact == "kernel-route"
|
|
else baseline.host_path.kernel_route_fingerprint
|
|
),
|
|
)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", lambda _target: changed_path)
|
|
monkeypatch.setattr(
|
|
service._host_wifi_association_probe, # noqa: SLF001
|
|
"observe",
|
|
lambda *_args, **_kwargs: {
|
|
"schema_version": 1,
|
|
"adapter": "CoreWLAN",
|
|
"wifi_interface": True,
|
|
"association_state": "unavailable",
|
|
"evidence_quality": "unavailable",
|
|
"continuity_proven": False,
|
|
"continuity_token": "d" * 64,
|
|
"reason_code": "association-identity-unavailable",
|
|
},
|
|
)
|
|
|
|
with pytest.raises(ApplicationConnectionBindingLost):
|
|
service._validate_application_connection_path(binding) # noqa: SLF001
|
|
|
|
changed = supervisor.snapshot()
|
|
assert changed.host_path.epoch == binding.host_path_epoch + 1
|
|
assert changed.authority.control_allowed is False
|
|
|
|
|
|
def test_command_timeout_crossing_host_ttl_refreshes_same_route_without_epoch_loss(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
monotonic_now = [100.0]
|
|
suspend_aware_now = [1_000.0]
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
supervisor._monotonic_clock = lambda: monotonic_now[0] # noqa: SLF001
|
|
supervisor._suspend_aware_clock = lambda: suspend_aware_now[0] # noqa: SLF001
|
|
supervisor._observation_ttl_seconds = 5.0 # noqa: SLF001
|
|
_control, binding, _ready = _install_authoritative_ready_bridge_control(service)
|
|
baseline = supervisor.snapshot()
|
|
target = EndpointTarget(binding.target_ipv4, binding.target_port)
|
|
|
|
monotonic_now[0] += 4.0
|
|
suspend_aware_now[0] += 4.0
|
|
assert supervisor.observe_endpoint(
|
|
target=target,
|
|
intent_id=binding.intent_id,
|
|
host_path_epoch=binding.host_path_epoch,
|
|
reachable=True,
|
|
)
|
|
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
|
|
def timeout_after_crossing_host_ttl(
|
|
_interface_name: str | None,
|
|
*,
|
|
timeout_seconds: float,
|
|
) -> HostWifiAssociationIdentityResult:
|
|
assert timeout_seconds == facade_module.COMMAND_BOUND_ASSOCIATION_TIMEOUT_SECONDS
|
|
monotonic_now[0] += 2.0
|
|
suspend_aware_now[0] += 2.0
|
|
return {
|
|
"schema_version": 1,
|
|
"adapter": "CoreWLAN",
|
|
"wifi_interface": True,
|
|
"association_state": "unavailable",
|
|
"evidence_quality": "unavailable",
|
|
"continuity_proven": False,
|
|
"continuity_token": "f" * 64,
|
|
"reason_code": "host-wifi-operation-timeout",
|
|
}
|
|
|
|
monkeypatch.setattr(
|
|
service._host_wifi_association_probe, # noqa: SLF001
|
|
"observe",
|
|
timeout_after_crossing_host_ttl,
|
|
)
|
|
|
|
service._validate_application_connection_path(binding) # noqa: SLF001
|
|
|
|
retained = supervisor.snapshot()
|
|
assert retained.host_path.epoch == baseline.host_path.epoch
|
|
assert retained.host_path.fingerprint == baseline.host_path.fingerprint
|
|
assert retained.host_path.available is True
|
|
assert retained.endpoint.tcp_state == "reachable"
|
|
assert retained.device_identity.state == "verified"
|
|
assert retained.control_plane.state == "healthy"
|
|
assert retained.lease.state == "reachable"
|
|
assert retained.authority.control_allowed is True
|
|
|
|
|
|
def test_configured_unverified_monitor_keeps_same_route_without_promoting_authority(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(service, with_control=False)
|
|
baseline = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
association_calls = 0
|
|
tcp_calls: list[str] = []
|
|
|
|
def timed_out_association(
|
|
_interface_name: str | None,
|
|
*,
|
|
timeout_seconds: float = 30.0,
|
|
) -> HostWifiAssociationIdentityResult:
|
|
nonlocal association_calls
|
|
assert timeout_seconds == facade_module.CONNECTION_MONITOR_ASSOCIATION_TIMEOUT_SECONDS
|
|
association_calls += 1
|
|
return {
|
|
"schema_version": 1,
|
|
"adapter": "CoreWLAN",
|
|
"wifi_interface": True,
|
|
"association_state": "unavailable",
|
|
"evidence_quality": "unavailable",
|
|
"continuity_proven": False,
|
|
"continuity_token": f"{association_calls:064x}",
|
|
"reason_code": "host-wifi-operation-timeout",
|
|
}
|
|
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(
|
|
service._host_wifi_association_probe, # noqa: SLF001
|
|
"observe",
|
|
timed_out_association,
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_probe_control_endpoint_socket",
|
|
lambda target: (
|
|
tcp_calls.append(target) or facade_module.TcpReachabilityProbeResult(reachable=True)
|
|
),
|
|
)
|
|
|
|
async def poll_five_times() -> list[facade_module.ConnectionSupervisorSnapshot]:
|
|
return [
|
|
await service._connection_monitor.poll_once() # noqa: SLF001
|
|
for _ in range(5)
|
|
]
|
|
|
|
snapshots = asyncio.run(poll_five_times())
|
|
|
|
assert association_calls == 10
|
|
assert tcp_calls == [binding.target_ipv4] * 5
|
|
for retained in snapshots:
|
|
assert retained.host_path.available is True
|
|
assert retained.host_path.epoch == baseline.host_path.epoch
|
|
assert retained.host_path.fingerprint == baseline.host_path.fingerprint
|
|
assert retained.endpoint.tcp_state == "reachable"
|
|
assert retained.device_identity.state == "unverified"
|
|
assert retained.control_plane.state != "healthy"
|
|
assert retained.lease.state == "configured-unverified"
|
|
assert retained.authority.control_allowed is False
|
|
assert retained.authority.acquisition_start_allowed is False
|
|
assert "verify-control-device-info" in retained.allowed_actions
|
|
|
|
|
|
def test_monitor_layer_retains_exact_configured_route_across_association_lock_timeout(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(service, with_control=False)
|
|
baseline = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
assert baseline.host_path.kernel_route_fingerprint is not None
|
|
tcp_calls: list[str] = []
|
|
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_sample_host_path",
|
|
lambda target, **_kwargs: HostPathProbeResult(
|
|
available=False,
|
|
fingerprint=None,
|
|
interface=baseline.host_path.interface,
|
|
source_ipv4=baseline.host_path.source_ipv4,
|
|
route_class="unavailable",
|
|
reason_code="host-wifi-operation-timeout",
|
|
observation_failure_class="association-observer",
|
|
kernel_route_fingerprint=baseline.host_path.kernel_route_fingerprint,
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_probe_control_endpoint_socket",
|
|
lambda target: (
|
|
tcp_calls.append(target)
|
|
or facade_module.TcpReachabilityProbeResult(reachable=True)
|
|
),
|
|
)
|
|
|
|
async def poll_three_times() -> list[facade_module.ConnectionSupervisorSnapshot]:
|
|
return [
|
|
await service._connection_monitor.poll_once() # noqa: SLF001
|
|
for _ in range(3)
|
|
]
|
|
|
|
snapshots = asyncio.run(poll_three_times())
|
|
|
|
assert tcp_calls == [binding.target_ipv4] * 3
|
|
for retained in snapshots:
|
|
assert retained.host_path.available is True
|
|
assert retained.host_path.epoch == baseline.host_path.epoch
|
|
assert retained.host_path.fingerprint == baseline.host_path.fingerprint
|
|
assert retained.endpoint.tcp_state == "reachable"
|
|
assert retained.device_identity.state == "unverified"
|
|
assert retained.authority.control_allowed is False
|
|
assert retained.authority.acquisition_start_allowed is False
|
|
assert "verify-control-device-info" in retained.allowed_actions
|
|
|
|
|
|
def test_association_timeout_cannot_hide_a_real_kernel_route_change(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_control, binding, _ready = _install_authoritative_ready_bridge_control(service)
|
|
baseline = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
changed_path = HostPathProbeResult(
|
|
available=True,
|
|
fingerprint="changed-kernel-route",
|
|
interface=baseline.host_path.interface,
|
|
source_ipv4=baseline.host_path.source_ipv4,
|
|
route_class="direct",
|
|
kernel_route_fingerprint="changed-kernel-route",
|
|
)
|
|
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", lambda _target: changed_path)
|
|
monkeypatch.setattr(
|
|
service._host_wifi_association_probe, # noqa: SLF001
|
|
"observe",
|
|
lambda *_args, **_kwargs: {
|
|
"schema_version": 1,
|
|
"adapter": "CoreWLAN",
|
|
"wifi_interface": True,
|
|
"association_state": "unavailable",
|
|
"evidence_quality": "unavailable",
|
|
"continuity_proven": False,
|
|
"continuity_token": "f" * 64,
|
|
"reason_code": "host-wifi-operation-timeout",
|
|
},
|
|
)
|
|
|
|
with pytest.raises(ApplicationConnectionBindingLost):
|
|
service._validate_application_connection_path(binding) # noqa: SLF001
|
|
|
|
changed = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
assert changed.host_path.epoch == baseline.host_path.epoch + 1
|
|
assert changed.host_path.kernel_route_fingerprint == "changed-kernel-route"
|
|
assert changed.authority.control_allowed is False
|
|
assert changed.lease.state != "reachable"
|
|
|
|
|
|
def test_command_preflight_drops_stale_sample_after_concurrent_route_change(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_control, binding, _ready = _install_authoritative_ready_bridge_control(service)
|
|
baseline = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
changed_path = HostPathProbeResult(
|
|
available=True,
|
|
fingerprint="changed-combined-route",
|
|
interface="changed0",
|
|
source_ipv4="192.168.99.2",
|
|
route_class="direct",
|
|
kernel_route_fingerprint="changed-kernel-route",
|
|
)
|
|
stale_path = HostPathProbeResult(
|
|
available=True,
|
|
fingerprint=baseline.host_path.fingerprint,
|
|
interface=baseline.host_path.interface,
|
|
source_ipv4=baseline.host_path.source_ipv4,
|
|
route_class="direct",
|
|
kernel_route_fingerprint=baseline.host_path.kernel_route_fingerprint,
|
|
)
|
|
|
|
def sample_after_newer_route_wins(
|
|
_target: str,
|
|
*,
|
|
association_timeout_seconds: float,
|
|
) -> HostPathProbeResult:
|
|
assert (
|
|
association_timeout_seconds
|
|
== facade_module.COMMAND_BOUND_ASSOCIATION_TIMEOUT_SECONDS
|
|
)
|
|
observed_epoch = service._connection_supervisor.observe_host_path( # noqa: SLF001
|
|
changed_path
|
|
)
|
|
assert observed_epoch == baseline.host_path.epoch + 1
|
|
return stale_path
|
|
|
|
monkeypatch.setattr(service, "_sample_host_path", sample_after_newer_route_wins)
|
|
|
|
with pytest.raises(ApplicationConnectionBindingLost):
|
|
service._validate_application_connection_path(binding) # noqa: SLF001
|
|
|
|
current = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
assert current.host_path.epoch == baseline.host_path.epoch + 1
|
|
assert current.host_path.fingerprint == "changed-combined-route"
|
|
assert current.host_path.kernel_route_fingerprint == "changed-kernel-route"
|
|
assert current.host_path.interface == "changed0"
|
|
assert current.host_path.source_ipv4 == "192.168.99.2"
|
|
assert current.authority.control_allowed is False
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"changed_route_fact",
|
|
["fingerprint", "interface", "source-ipv4"],
|
|
)
|
|
def test_monitor_does_not_debounce_real_route_change_hidden_by_observer_failure(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
changed_route_fact: str,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_control, _binding, _ready = _install_authoritative_ready_bridge_control(service)
|
|
baseline = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
baseline_path = baseline.host_path
|
|
assert baseline_path.kernel_route_fingerprint is not None
|
|
changed = HostPathProbeResult(
|
|
available=False,
|
|
fingerprint=None,
|
|
interface=("changed0" if changed_route_fact == "interface" else baseline_path.interface),
|
|
source_ipv4=(
|
|
"192.168.99.2" if changed_route_fact == "source-ipv4" else baseline_path.source_ipv4
|
|
),
|
|
route_class="unavailable",
|
|
reason_code="host-wifi-operation-timeout",
|
|
observation_failure_class="association-observer",
|
|
kernel_route_fingerprint=(
|
|
"changed-kernel-route"
|
|
if changed_route_fact == "fingerprint"
|
|
else baseline_path.kernel_route_fingerprint
|
|
),
|
|
)
|
|
monkeypatch.setattr(service, "_sample_host_path", lambda *_args, **_kwargs: changed)
|
|
tcp_samples: list[str] = []
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_probe_control_endpoint_socket",
|
|
lambda target: (
|
|
tcp_samples.append(target) or facade_module.TcpReachabilityProbeResult(reachable=True)
|
|
),
|
|
)
|
|
|
|
observed = asyncio.run(service._connection_monitor.poll_once()) # noqa: SLF001
|
|
|
|
assert observed.revision > baseline.revision
|
|
assert observed.host_path.available is False
|
|
assert observed.host_path.reason_code == "host-wifi-operation-timeout"
|
|
assert observed.authority.control_allowed is False
|
|
assert observed.lease.state != "reachable"
|
|
assert tcp_samples == []
|
|
|
|
|
|
def test_read_only_monitor_runs_alongside_steady_control_holder(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_control, binding, _ready = _install_authoritative_ready_bridge_control(service)
|
|
initial = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
host_samples: list[str] = []
|
|
tcp_samples: list[str] = []
|
|
|
|
def sample_host(
|
|
target: str,
|
|
*,
|
|
association_timeout_seconds: float = 30.0,
|
|
fail_closed_unproven_association: bool = False,
|
|
) -> HostPathProbeResult:
|
|
assert association_timeout_seconds == (
|
|
facade_module.CONNECTION_MONITOR_ASSOCIATION_TIMEOUT_SECONDS
|
|
)
|
|
assert fail_closed_unproven_association is True
|
|
host_samples.append(target)
|
|
return _direct_host_path(target)
|
|
|
|
def sample_tcp(target: str) -> facade_module.TcpReachabilityProbeResult:
|
|
tcp_samples.append(target)
|
|
return facade_module.TcpReachabilityProbeResult(reachable=True)
|
|
|
|
monkeypatch.setattr(service, "_sample_host_path", sample_host)
|
|
monkeypatch.setattr(facade_module, "_probe_control_endpoint_socket", sample_tcp)
|
|
|
|
refreshed = asyncio.run(service._connection_monitor.poll_once()) # noqa: SLF001
|
|
|
|
assert host_samples == [binding.target_ipv4, binding.target_ipv4]
|
|
assert tcp_samples == [binding.target_ipv4]
|
|
assert refreshed.revision > initial.revision
|
|
assert refreshed.host_path.epoch == initial.host_path.epoch
|
|
assert refreshed.endpoint.tcp_state == "reachable"
|
|
assert service._application_control_process_lease_holders == {"control"} # noqa: SLF001
|
|
|
|
|
|
def test_read_only_monitor_keeps_ready_from_exact_durable_target_after_ephemeral_cleanup(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_control, binding, _ready = _install_authoritative_ready_bridge_control(
|
|
service,
|
|
transport_ref="F89438FA-55ED-85AD-EED7-734AC84746D8",
|
|
)
|
|
_seed_durable_bridge_topology(
|
|
service,
|
|
# CoreBluetooth UUID spelling is not durable identity. The monitor
|
|
# compares its physical key case-insensitively while retaining the
|
|
# exact supervisor spelling for current-process evidence.
|
|
transport_ref=binding.transport_ref.lower(),
|
|
ipv4=binding.target_ipv4,
|
|
)
|
|
service._retire_ephemeral_device_binding_for_new_intent() # noqa: SLF001
|
|
host_samples: list[str] = []
|
|
tcp_samples: list[str] = []
|
|
|
|
def sample_host(
|
|
target: str,
|
|
**_: object,
|
|
) -> HostPathProbeResult:
|
|
host_samples.append(target)
|
|
return _direct_host_path(target)
|
|
|
|
def sample_tcp(target: str) -> facade_module.TcpReachabilityProbeResult:
|
|
tcp_samples.append(target)
|
|
return facade_module.TcpReachabilityProbeResult(reachable=True)
|
|
|
|
monkeypatch.setattr(service, "_sample_host_path", sample_host)
|
|
monkeypatch.setattr(facade_module, "_probe_control_endpoint_socket", sample_tcp)
|
|
|
|
refreshed = asyncio.run(service._connection_monitor.poll_once()) # noqa: SLF001
|
|
|
|
assert service._selected_device_id is None # noqa: SLF001
|
|
assert service._device_session_id is None # noqa: SLF001
|
|
assert host_samples == [binding.target_ipv4, binding.target_ipv4]
|
|
assert tcp_samples == [binding.target_ipv4]
|
|
assert refreshed.host_path.available is True
|
|
assert refreshed.endpoint.tcp_state == "reachable"
|
|
assert refreshed.device_identity.state == "verified"
|
|
assert refreshed.control_plane.state == "healthy"
|
|
assert refreshed.lease.state == "reachable"
|
|
assert refreshed.authority.control_allowed is True
|
|
assert refreshed.authority.acquisition_start_allowed is True
|
|
|
|
|
|
def test_read_only_monitor_durable_target_still_downgrades_on_real_route_loss(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_control, binding, _ready = _install_authoritative_ready_bridge_control(service)
|
|
_seed_durable_bridge_topology(
|
|
service,
|
|
transport_ref=binding.transport_ref,
|
|
ipv4=binding.target_ipv4,
|
|
)
|
|
service._retire_ephemeral_device_binding_for_new_intent() # noqa: SLF001
|
|
tcp_samples: list[str] = []
|
|
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_sample_host_path",
|
|
lambda _target, **_kwargs: HostPathProbeResult(
|
|
available=False,
|
|
fingerprint=None,
|
|
interface=None,
|
|
source_ipv4=None,
|
|
route_class="unavailable",
|
|
reason_code="router-link-lost",
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_probe_control_endpoint_socket",
|
|
lambda target: (
|
|
tcp_samples.append(target) or facade_module.TcpReachabilityProbeResult(reachable=True)
|
|
),
|
|
)
|
|
|
|
lost = asyncio.run(service._connection_monitor.poll_once()) # noqa: SLF001
|
|
|
|
assert tcp_samples == []
|
|
assert lost.host_path.available is False
|
|
assert lost.host_path.reason_code == "router-link-lost"
|
|
assert lost.endpoint.tcp_state == "unknown"
|
|
assert lost.lease.state == "configured-unverified"
|
|
assert lost.authority.control_allowed is False
|
|
assert lost.authority.acquisition_start_allowed is False
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"invalid_context",
|
|
[
|
|
"selected-without-session",
|
|
"session-without-selected",
|
|
"semantic-empty",
|
|
"semantic-store-missing",
|
|
"semantic-store-corrupt",
|
|
"semantic-ref-mismatch",
|
|
"semantic-mode-mismatch",
|
|
"semantic-ip-mismatch",
|
|
"semantic-profile-mismatch",
|
|
"physically-retired",
|
|
],
|
|
)
|
|
def test_read_only_monitor_durable_fallback_rejects_inexact_or_unsafe_context(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
invalid_context: str,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_control, binding, _ready = _install_authoritative_ready_bridge_control(service)
|
|
store = service._semantic_topology_store # noqa: SLF001
|
|
assert store is not None
|
|
|
|
contexts_with_exact_semantic = {
|
|
"selected-without-session",
|
|
"session-without-selected",
|
|
"semantic-store-missing",
|
|
"semantic-store-corrupt",
|
|
"physically-retired",
|
|
}
|
|
if invalid_context in contexts_with_exact_semantic:
|
|
_seed_durable_bridge_topology(
|
|
service,
|
|
transport_ref=binding.transport_ref,
|
|
ipv4=binding.target_ipv4,
|
|
)
|
|
elif invalid_context.startswith("semantic-") and invalid_context != "semantic-empty":
|
|
snapshot = store.snapshot()
|
|
store.commit(
|
|
transport_ref=(
|
|
"different-k1"
|
|
if invalid_context == "semantic-ref-mismatch"
|
|
else binding.transport_ref
|
|
),
|
|
connection_mode=(
|
|
"direct-connect" if invalid_context == "semantic-mode-mismatch" else "bridge"
|
|
),
|
|
ipv4=(
|
|
"192.168.68.77"
|
|
if invalid_context == "semantic-ip-mismatch"
|
|
else binding.target_ipv4
|
|
),
|
|
compatibility_profile_id=(
|
|
"different.compatibility.profile"
|
|
if invalid_context == "semantic-profile-mismatch"
|
|
else XGRIDS_K1_COMPATIBILITY_PROFILE_ID
|
|
),
|
|
firmware_version="3.0.2",
|
|
source="ble-read-only-status",
|
|
observed_at_utc="2026-08-11T08:00:00Z",
|
|
predecessor_revision=(snapshot.record.revision if snapshot.record is not None else 0),
|
|
)
|
|
|
|
service._retire_ephemeral_device_binding_for_new_intent() # noqa: SLF001
|
|
if invalid_context == "selected-without-session":
|
|
with service._lock: # noqa: SLF001
|
|
service._selected_device_id = binding.transport_ref # noqa: SLF001
|
|
elif invalid_context == "session-without-selected":
|
|
with service._lock: # noqa: SLF001
|
|
service._device_session_id = "orphaned-session" # noqa: SLF001
|
|
elif invalid_context == "semantic-store-missing":
|
|
service._semantic_topology_store = None # noqa: SLF001
|
|
elif invalid_context == "semantic-store-corrupt":
|
|
|
|
def corrupt_snapshot() -> object:
|
|
raise facade_module.SemanticTopologyStoreCorrupt("synthetic corrupt store")
|
|
|
|
monkeypatch.setattr(store, "snapshot", corrupt_snapshot)
|
|
elif invalid_context == "physically-retired":
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_retired_physical_transport_refs",
|
|
lambda: {binding.transport_ref.casefold()},
|
|
)
|
|
|
|
host_samples: list[str] = []
|
|
tcp_samples: list[str] = []
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_sample_host_path",
|
|
lambda target, **_kwargs: host_samples.append(target) or _direct_host_path(target),
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_probe_control_endpoint_socket",
|
|
lambda target: (
|
|
tcp_samples.append(target) or facade_module.TcpReachabilityProbeResult(reachable=True)
|
|
),
|
|
)
|
|
|
|
assert service._connection_monitor_target() is None # noqa: SLF001
|
|
rejected = asyncio.run(service._connection_monitor.poll_once()) # noqa: SLF001
|
|
|
|
assert host_samples == []
|
|
assert tcp_samples == []
|
|
assert rejected.host_path.available is False
|
|
assert rejected.host_path.reason_code == "endpoint-target-unconfigured"
|
|
assert rejected.endpoint.tcp_state == "unknown"
|
|
assert rejected.lease.state == "configured-unverified"
|
|
assert rejected.authority.control_allowed is False
|
|
assert rejected.authority.acquisition_start_allowed is False
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"durable_store_change",
|
|
["missing", "corrupt", "replaced"],
|
|
)
|
|
def test_monitor_revalidates_durable_sourced_supervisor_even_with_ephemeral_binding(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
durable_store_change: str,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_control, binding, _ready = _install_authoritative_ready_bridge_control(service)
|
|
_seed_durable_bridge_topology(
|
|
service,
|
|
transport_ref=binding.transport_ref,
|
|
ipv4=binding.target_ipv4,
|
|
)
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
current = supervisor.snapshot()
|
|
assert current.intent is not None
|
|
assert current.device_network.target is not None
|
|
assert supervisor.observe_device_network_applied(
|
|
intent_id=current.intent.intent_id,
|
|
transport_ref=binding.transport_ref,
|
|
connection_mode="bridge",
|
|
target=current.device_network.target,
|
|
source="durable-semantic-topology",
|
|
)
|
|
assert service._connection_monitor_target() == current.device_network.target # noqa: SLF001
|
|
|
|
store = service._semantic_topology_store # noqa: SLF001
|
|
assert store is not None
|
|
if durable_store_change == "missing":
|
|
service._semantic_topology_store = None # noqa: SLF001
|
|
elif durable_store_change == "corrupt":
|
|
|
|
def corrupt_snapshot() -> object:
|
|
raise facade_module.SemanticTopologyStoreCorrupt("synthetic corrupt store")
|
|
|
|
monkeypatch.setattr(store, "snapshot", corrupt_snapshot)
|
|
else:
|
|
semantic = store.snapshot()
|
|
assert semantic.record is not None
|
|
store.commit(
|
|
transport_ref=binding.transport_ref,
|
|
connection_mode="bridge",
|
|
ipv4="192.168.68.77",
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
firmware_version="3.0.2",
|
|
source="ble-read-only-status",
|
|
observed_at_utc="2026-08-11T08:30:00Z",
|
|
predecessor_revision=semantic.record.revision,
|
|
)
|
|
|
|
host_samples: list[str] = []
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_sample_host_path",
|
|
lambda target, **_kwargs: host_samples.append(target) or _direct_host_path(target),
|
|
)
|
|
|
|
assert service._connection_monitor_target() is None # noqa: SLF001
|
|
rejected = asyncio.run(service._connection_monitor.poll_once()) # noqa: SLF001
|
|
|
|
assert host_samples == []
|
|
assert rejected.host_path.available is False
|
|
assert rejected.host_path.reason_code == "endpoint-target-unconfigured"
|
|
assert rejected.authority.control_allowed is False
|
|
|
|
|
|
def test_monitor_network_admission_race_is_dropped_without_negative_evidence(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_control, _binding, _ready = _install_authoritative_ready_bridge_control(service)
|
|
initial = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
original_acquire = service._acquire_k1_lifecycle_process_lease # noqa: SLF001
|
|
network_injected = False
|
|
host_io: list[str] = []
|
|
|
|
def raced_acquire(holder: Any) -> None:
|
|
nonlocal network_injected
|
|
if holder == "monitor" and not network_injected:
|
|
original_acquire("network")
|
|
network_injected = True
|
|
original_acquire(holder)
|
|
|
|
monkeypatch.setattr(service, "_acquire_k1_lifecycle_process_lease", raced_acquire)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_sample_host_path",
|
|
lambda *_args, **_kwargs: host_io.append("host") or _direct_host_path("10.0.0.1"),
|
|
)
|
|
|
|
try:
|
|
after_race = asyncio.run(service._connection_monitor.poll_once()) # noqa: SLF001
|
|
finally:
|
|
if network_injected:
|
|
service._release_k1_lifecycle_process_lease("network") # noqa: SLF001
|
|
|
|
assert network_injected is True
|
|
assert host_io == []
|
|
assert after_race.revision == initial.revision
|
|
assert after_race.host_path == initial.host_path
|
|
assert after_race.endpoint == initial.endpoint
|
|
assert service._application_control_process_lease_holders == {"control"} # noqa: SLF001
|
|
|
|
|
|
def test_explicit_reconfiguration_waits_for_monitor_and_drops_stale_probe(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, binding, ready = _install_authoritative_ready_bridge_control(service)
|
|
request = _reconfiguration_request(ready, "select-device")
|
|
initial_supervisor = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
probe_entered = threading.Event()
|
|
release_probe = threading.Event()
|
|
device_edges: list[str] = []
|
|
|
|
def blocked_host_path(
|
|
target: str,
|
|
*,
|
|
association_timeout_seconds: float = 30.0,
|
|
fail_closed_unproven_association: bool = False,
|
|
) -> HostPathProbeResult:
|
|
assert association_timeout_seconds == (
|
|
facade_module.CONNECTION_MONITOR_ASSOCIATION_TIMEOUT_SECONDS
|
|
)
|
|
assert fail_closed_unproven_association is True
|
|
probe_entered.set()
|
|
assert release_probe.wait(timeout=5.0)
|
|
assert target == binding.target_ipv4
|
|
# If this superseded result were admitted after the operator won the
|
|
# transition, it would rotate the host epoch and revoke the old proof.
|
|
return _direct_host_path("192.168.56.20")
|
|
|
|
async def forbidden_device_io(*_args: object, **_kwargs: object) -> object:
|
|
device_edges.append("ble-or-network")
|
|
raise AssertionError("local reconfiguration must not perform device I/O")
|
|
|
|
monkeypatch.setattr(service, "_sample_host_path", blocked_host_path)
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_device_io)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_device_io)
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"prepare",
|
|
lambda *_args, **_kwargs: device_edges.append("physical-command"),
|
|
)
|
|
|
|
async def scenario() -> tuple[dict[str, Any], dict[str, Any]]:
|
|
monitor_task = asyncio.create_task(
|
|
service._connection_monitor.poll_once() # noqa: SLF001
|
|
)
|
|
assert await asyncio.to_thread(probe_entered.wait, 5.0)
|
|
|
|
during_probe = await asyncio.to_thread(service.state)
|
|
assert "monitor" in during_probe["k1_lifecycle_process_lease"]["holders"]
|
|
for action in ("prepare-select-device", "prepare-change-network"):
|
|
assert during_probe["connection_policy"]["actions"][action]["allowed"] is True
|
|
|
|
prepare_task = asyncio.create_task(service.prepare_connection_reconfiguration(request))
|
|
for _ in range(100):
|
|
if service._k1_lifecycle_transition_gate.locked(): # noqa: SLF001
|
|
break
|
|
await asyncio.sleep(0.001)
|
|
assert service._k1_lifecycle_transition_gate.locked() # noqa: SLF001
|
|
await asyncio.sleep(0.01)
|
|
assert prepare_task.done() is False
|
|
|
|
release_probe.set()
|
|
monitor_state = await monitor_task
|
|
prepared = await prepare_task
|
|
return monitor_state, prepared
|
|
|
|
monitor_state, prepared = asyncio.run(scenario())
|
|
|
|
assert monitor_state.host_path.epoch == initial_supervisor.host_path.epoch
|
|
assert prepared["connection_reconfiguration"]["intent"] == "select-device"
|
|
assert prepared["connection_reconfiguration"]["status"] == "awaiting-fresh-scan"
|
|
assert prepared["connection_supervisor"]["observed"]["host_path"]["epoch"] == (
|
|
initial_supervisor.host_path.epoch
|
|
)
|
|
assert "monitor" not in service._application_control_process_lease_holders # noqa: SLF001
|
|
assert control.stop_calls == 0
|
|
assert control.start_projects == []
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
assert device_edges == []
|
|
assert binding.target_ipv4 == "192.168.1.20"
|
|
|
|
|
|
def test_cancelled_monitor_keeps_contact_owned_until_operator_can_commit(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, binding, ready = _install_authoritative_ready_bridge_control(service)
|
|
request = _reconfiguration_request(ready, "select-device")
|
|
initial = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
probe_entered = threading.Event()
|
|
release_probe = threading.Event()
|
|
device_edges: list[str] = []
|
|
|
|
def blocked_host_path(
|
|
target: str,
|
|
*,
|
|
association_timeout_seconds: float = 30.0,
|
|
fail_closed_unproven_association: bool = False,
|
|
) -> HostPathProbeResult:
|
|
assert association_timeout_seconds == (
|
|
facade_module.CONNECTION_MONITOR_ASSOCIATION_TIMEOUT_SECONDS
|
|
)
|
|
assert fail_closed_unproven_association is True
|
|
probe_entered.set()
|
|
assert release_probe.wait(timeout=5.0)
|
|
assert target == binding.target_ipv4
|
|
return _direct_host_path("192.168.56.20")
|
|
|
|
async def forbidden_device_io(*_args: object, **_kwargs: object) -> object:
|
|
device_edges.append("ble-or-network")
|
|
raise AssertionError("cancelled monitor handoff must not perform device I/O")
|
|
|
|
monkeypatch.setattr(service, "_sample_host_path", blocked_host_path)
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_device_io)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_device_io)
|
|
|
|
async def scenario() -> tuple[dict[str, Any], bool]:
|
|
monitor_task = asyncio.create_task(
|
|
service._connection_monitor.poll_once() # noqa: SLF001
|
|
)
|
|
assert await asyncio.to_thread(probe_entered.wait, 5.0)
|
|
monitor_task.cancel()
|
|
|
|
prepare_task = asyncio.create_task(service.prepare_connection_reconfiguration(request))
|
|
for _ in range(100):
|
|
if service._k1_lifecycle_transition_gate.locked(): # noqa: SLF001
|
|
break
|
|
await asyncio.sleep(0.001)
|
|
assert service._k1_lifecycle_transition_gate.locked() # noqa: SLF001
|
|
await asyncio.sleep(0.01)
|
|
assert prepare_task.done() is False
|
|
assert "network" not in service._application_control_process_lease_holders # noqa: SLF001
|
|
|
|
release_probe.set()
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await monitor_task
|
|
prepared = await prepare_task
|
|
return prepared, service._connection_monitor_contact_gate.locked() # noqa: SLF001
|
|
|
|
prepared, contact_locked = asyncio.run(scenario())
|
|
|
|
assert contact_locked is False
|
|
assert service._k1_lifecycle_transition_gate.locked() is False # noqa: SLF001
|
|
assert service._connection_reconfiguration_gate.locked() is False # noqa: SLF001
|
|
assert prepared["connection_reconfiguration"]["status"] == "awaiting-fresh-scan"
|
|
assert prepared["connection_supervisor"]["observed"]["host_path"]["epoch"] == (
|
|
initial.host_path.epoch
|
|
)
|
|
assert "monitor" not in service._application_control_process_lease_holders # noqa: SLF001
|
|
assert control.stop_calls == 0
|
|
assert control.start_projects == []
|
|
assert runtime.start_calls == []
|
|
assert device_edges == []
|
|
|
|
|
|
def test_monitor_quiescence_timeout_releases_operator_gates_and_drops_late_result(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, binding, ready = _install_authoritative_ready_bridge_control(service)
|
|
request = _reconfiguration_request(ready, "select-device")
|
|
initial = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
probe_entered = threading.Event()
|
|
release_probe = threading.Event()
|
|
device_edges: list[str] = []
|
|
|
|
def blocked_host_path(
|
|
target: str,
|
|
*,
|
|
association_timeout_seconds: float = 30.0,
|
|
fail_closed_unproven_association: bool = False,
|
|
) -> HostPathProbeResult:
|
|
assert association_timeout_seconds == (
|
|
facade_module.CONNECTION_MONITOR_ASSOCIATION_TIMEOUT_SECONDS
|
|
)
|
|
assert fail_closed_unproven_association is True
|
|
probe_entered.set()
|
|
assert release_probe.wait(timeout=5.0)
|
|
assert target == binding.target_ipv4
|
|
return _direct_host_path("192.168.56.20")
|
|
|
|
async def forbidden_device_io(*_args: object, **_kwargs: object) -> object:
|
|
device_edges.append("ble-or-network")
|
|
raise AssertionError("quiescence timeout must not perform device I/O")
|
|
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"CONNECTION_MONITOR_QUIESCE_TIMEOUT_SECONDS",
|
|
0.01,
|
|
)
|
|
monkeypatch.setattr(service, "_sample_host_path", blocked_host_path)
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_device_io)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_device_io)
|
|
|
|
async def scenario() -> None:
|
|
monitor_task = asyncio.create_task(
|
|
service._connection_monitor.poll_once() # noqa: SLF001
|
|
)
|
|
assert await asyncio.to_thread(probe_entered.wait, 5.0)
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as conflict:
|
|
await service.prepare_connection_reconfiguration(request)
|
|
assert conflict.value.reason_code == "connection-reconfiguration-lifecycle-busy"
|
|
assert service._k1_lifecycle_transition_gate.locked() is False # noqa: SLF001
|
|
assert service._connection_reconfiguration_gate.locked() is False # noqa: SLF001
|
|
assert "network" not in service._application_control_process_lease_holders # noqa: SLF001
|
|
|
|
monitor_task.cancel()
|
|
release_probe.set()
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await monitor_task
|
|
|
|
asyncio.run(scenario())
|
|
|
|
after = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
assert after.revision == initial.revision
|
|
assert after.host_path == initial.host_path
|
|
assert service._connection_monitor_contact_gate.locked() is False # noqa: SLF001
|
|
assert service._connection_reconfiguration_intent is None # noqa: SLF001
|
|
assert service._application_control_process_lease_holders == {"control"} # noqa: SLF001
|
|
assert control.stop_calls == 0
|
|
assert control.start_projects == []
|
|
assert runtime.start_calls == []
|
|
assert device_edges == []
|
|
|
|
|
|
def test_same_mode_scan_waits_for_monitor_instead_of_returning_busy(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _runtime = service_with_fake_runtime(tmp_path)
|
|
_control, binding, ready = _install_authoritative_ready_bridge_control(service)
|
|
initial_epoch = ready["connection_supervisor"]["observed"]["host_path"]["epoch"]
|
|
probe_entered = threading.Event()
|
|
release_probe = threading.Event()
|
|
scan_calls: list[float] = []
|
|
tcp_calls: list[str] = []
|
|
|
|
def blocked_host_path(
|
|
target: str,
|
|
*,
|
|
association_timeout_seconds: float = 30.0,
|
|
fail_closed_unproven_association: bool = False,
|
|
) -> HostPathProbeResult:
|
|
assert association_timeout_seconds == (
|
|
facade_module.CONNECTION_MONITOR_ASSOCIATION_TIMEOUT_SECONDS
|
|
)
|
|
assert fail_closed_unproven_association is True
|
|
probe_entered.set()
|
|
assert release_probe.wait(timeout=5.0)
|
|
assert target == binding.target_ipv4
|
|
return _direct_host_path("192.168.56.20")
|
|
|
|
async def fake_scan(
|
|
duration_seconds: float,
|
|
*,
|
|
on_admitted: Callable[[], None] | None = None,
|
|
) -> dict[str, Any]:
|
|
scan_calls.append(duration_seconds)
|
|
assert on_admitted is not None
|
|
on_admitted()
|
|
return _ble_scan_result("fresh-k1")
|
|
|
|
def forbidden_tcp(target: str) -> facade_module.TcpReachabilityProbeResult:
|
|
tcp_calls.append(target)
|
|
raise AssertionError("operator lifecycle gate must supersede the next monitor contact")
|
|
|
|
monkeypatch.setattr(service, "_sample_host_path", blocked_host_path)
|
|
monkeypatch.setattr(facade_module, "_probe_control_endpoint_socket", forbidden_tcp)
|
|
monkeypatch.setattr(facade_module, "scan", fake_scan)
|
|
|
|
async def scenario() -> tuple[dict[str, Any], int]:
|
|
monitor_task = asyncio.create_task(
|
|
service._connection_monitor.poll_once() # noqa: SLF001
|
|
)
|
|
assert await asyncio.to_thread(probe_entered.wait, 5.0)
|
|
scan_task = asyncio.create_task(service.scan_ble(0.1))
|
|
for _ in range(100):
|
|
if service._k1_lifecycle_transition_gate.locked(): # noqa: SLF001
|
|
break
|
|
await asyncio.sleep(0.001)
|
|
assert service._k1_lifecycle_transition_gate.locked() # noqa: SLF001
|
|
await asyncio.sleep(0.01)
|
|
assert scan_task.done() is False
|
|
assert scan_calls == []
|
|
assert "network" not in service._application_control_process_lease_holders # noqa: SLF001
|
|
|
|
release_probe.set()
|
|
monitor_state = await monitor_task
|
|
scanned = await scan_task
|
|
return scanned, monitor_state.host_path.epoch
|
|
|
|
scanned, monitor_epoch = asyncio.run(scenario())
|
|
|
|
assert scan_calls == [0.1]
|
|
assert [item["device_id"] for item in scanned["devices"]] == ["fresh-k1"]
|
|
assert monitor_epoch == initial_epoch
|
|
assert scanned["connection_supervisor"]["observed"]["host_path"]["epoch"] == (initial_epoch)
|
|
assert service._connection_monitor_contact_gate.locked() is False # noqa: SLF001
|
|
assert "monitor" not in service._application_control_process_lease_holders # noqa: SLF001
|
|
assert tcp_calls == []
|
|
|
|
|
|
def test_select_device_handoff_is_local_cancel_invalidates_candidates_and_stale_tabs(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, _binding, ready = _install_authoritative_ready_bridge_control(service)
|
|
initial_request = _reconfiguration_request(ready, "select-device")
|
|
semantic_before = ready["semantic_topology_store"]
|
|
identity_before = ready["device_identity_pin_store"]
|
|
device_edges: list[str] = []
|
|
|
|
async def forbidden_network_write(*_: object, **__: object) -> dict[str, Any]:
|
|
device_edges.append("network-write")
|
|
raise AssertionError("local reconfiguration handoff must not write K1")
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_network_write)
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"prepare",
|
|
lambda *_args, **_kwargs: device_edges.append("physical-command"),
|
|
)
|
|
|
|
prepared = asyncio.run(service.prepare_connection_reconfiguration(initial_request))
|
|
|
|
assert prepared["connection_reconfiguration"]["intent"] == "select-device"
|
|
assert prepared["connection_reconfiguration"]["status"] == "awaiting-fresh-scan"
|
|
assert prepared["connection_reconfiguration"]["required_connection_mode"] == "bridge"
|
|
assert prepared["connection_lifecycle"]["active_binding"] is None
|
|
assert prepared["selected_device_id"] is None
|
|
assert (
|
|
prepared["connection_policy"]["actions"]["prepare-select-device"][
|
|
"required_connection_mode"
|
|
]
|
|
== "bridge"
|
|
)
|
|
assert prepared["semantic_topology_store"] == semantic_before
|
|
assert prepared["device_identity_pin_store"] == identity_before
|
|
assert control.state == "idle"
|
|
assert control.start_projects == []
|
|
assert control.stop_calls == 0
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
assert service._application_control_process_lease_holders == set() # noqa: SLF001
|
|
assert device_edges == []
|
|
|
|
# The losing tab cannot repeat the original handoff after the stable
|
|
# operator-only revision/intent id advanced.
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as stale_prepare:
|
|
asyncio.run(service.prepare_connection_reconfiguration(initial_request))
|
|
assert stale_prepare.value.reason_code == "connection-reconfiguration-revision-conflict"
|
|
|
|
minimum_generation = prepared["connection_reconfiguration"]["minimum_discovery_generation"]
|
|
service._ble_discovery_generation = minimum_generation # noqa: SLF001
|
|
_set_scanned_k1(service, device_id="k1-b")
|
|
fresh = service.state()
|
|
assert fresh["connection_reconfiguration"]["status"] == "fresh-scan-completed"
|
|
assert [item["device_id"] for item in fresh["devices"]] == ["k1-b"]
|
|
stale_connect_request = _connect_request(
|
|
device_id="k1-b",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
connection_mode="bridge",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_mode_revision=fresh["desired_connection_mode_revision"],
|
|
expected_discovery_generation=fresh["ble_discovery_generation"],
|
|
expected_reconfiguration_revision=fresh["connection_reconfiguration"]["revision"],
|
|
expected_reconfiguration_intent_id=fresh["connection_reconfiguration"]["intent_id"],
|
|
)
|
|
stale_verify_request = ConnectionVerifyRequest(
|
|
device_id="k1-b",
|
|
source="fresh-scan",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_discovery_generation=fresh["ble_discovery_generation"],
|
|
expected_reconfiguration_revision=fresh["connection_reconfiguration"]["revision"],
|
|
expected_reconfiguration_intent_id=fresh["connection_reconfiguration"]["intent_id"],
|
|
)
|
|
quick_verify_request = ConnectionVerifyRequest(
|
|
device_id="k1-b",
|
|
source="fresh-scan",
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
expected_discovery_generation=fresh["ble_discovery_generation"],
|
|
expected_reconfiguration_revision=fresh["connection_reconfiguration"]["revision"],
|
|
expected_reconfiguration_intent_id=fresh["connection_reconfiguration"]["intent_id"],
|
|
)
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as quick_isolated:
|
|
asyncio.run(service.verify_connection(quick_verify_request))
|
|
assert quick_isolated.value.reason_code == "connection-reconfiguration-target-mismatch"
|
|
cancel_request = _reconfiguration_request(fresh, "cancel")
|
|
|
|
cancelled = asyncio.run(service.prepare_connection_reconfiguration(cancel_request))
|
|
|
|
assert cancelled["connection_reconfiguration"]["status"] == "idle"
|
|
assert cancelled["connection_reconfiguration"]["intent_id"] is None
|
|
assert cancelled["devices"] == []
|
|
assert cancelled["ble_discovery_generation"] > minimum_generation
|
|
assert cancelled["semantic_topology_store"] == semantic_before
|
|
assert cancelled["device_identity_pin_store"] == identity_before
|
|
assert device_edges == []
|
|
|
|
password_reads: list[str] = []
|
|
process_lease_calls: list[str] = []
|
|
|
|
def forbidden_password_read(_secret: SecretStr) -> str:
|
|
password_reads.append("unwrapped")
|
|
raise AssertionError("stale Connect must fail before password unwrap")
|
|
|
|
def forbidden_process_lease(holder: str) -> None:
|
|
process_lease_calls.append(holder)
|
|
raise AssertionError("stale mutation must fail before process lease")
|
|
|
|
monkeypatch.setattr(SecretStr, "get_secret_value", forbidden_password_read)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_acquire_k1_lifecycle_process_lease",
|
|
forbidden_process_lease,
|
|
)
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as stale_connect:
|
|
asyncio.run(service.connect(stale_connect_request))
|
|
assert stale_connect.value.reason_code == "connection-reconfiguration-revision-conflict"
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as stale_verify:
|
|
asyncio.run(service.verify_connection(stale_verify_request))
|
|
assert stale_verify.value.reason_code == "connection-reconfiguration-revision-conflict"
|
|
assert password_reads == []
|
|
assert process_lease_calls == []
|
|
assert service._operations.snapshot() == [] # noqa: SLF001
|
|
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as stale_cancel:
|
|
asyncio.run(service.prepare_connection_reconfiguration(cancel_request))
|
|
assert stale_cancel.value.reason_code == "connection-reconfiguration-revision-conflict"
|
|
|
|
|
|
def test_change_network_requires_authoritative_binding_before_any_local_teardown(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
control, _binding, ready = _install_authoritative_ready_bridge_control(service)
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
supervisor_state = supervisor.snapshot()
|
|
assert supervisor_state.intent is not None
|
|
assert supervisor_state.device_network.target is not None
|
|
assert supervisor.observe_endpoint(
|
|
target=supervisor_state.device_network.target,
|
|
intent_id=supervisor_state.intent.intent_id,
|
|
host_path_epoch=supervisor_state.host_path.epoch,
|
|
reachable=False,
|
|
reason_code="tcp-endpoint-unreachable",
|
|
)
|
|
degraded = service.state()
|
|
assert degraded["selected_device_id"] == "k1-a"
|
|
assert degraded["connection_lifecycle"]["active_binding"] is None
|
|
assert control.state == "connection-ready"
|
|
decision = degraded["connection_policy"]["actions"]["prepare-change-network"]
|
|
assert decision["allowed"] is False
|
|
assert "connection-reconfiguration-current-device-unavailable" in decision["reason_codes"]
|
|
|
|
process_lease_calls: list[str] = []
|
|
original_acquire = service._acquire_k1_lifecycle_process_lease # noqa: SLF001
|
|
|
|
def counted_acquire(holder: str) -> None:
|
|
process_lease_calls.append(holder)
|
|
original_acquire(holder)
|
|
|
|
monkeypatch.setattr(service, "_acquire_k1_lifecycle_process_lease", counted_acquire)
|
|
before_control_revision = control.state_revision
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as unavailable:
|
|
asyncio.run(
|
|
service.prepare_connection_reconfiguration(
|
|
_reconfiguration_request(degraded, "change-network")
|
|
)
|
|
)
|
|
|
|
assert unavailable.value.reason_code == (
|
|
"connection-reconfiguration-current-device-unavailable"
|
|
)
|
|
assert process_lease_calls == []
|
|
assert control.state == "connection-ready"
|
|
assert control.state_revision == before_control_revision
|
|
assert service._selected_device_id == "k1-a" # noqa: SLF001
|
|
assert service._connection_reconfiguration_intent is None # noqa: SLF001
|
|
assert service._application_control_process_lease_holders == {"control"} # noqa: SLF001
|
|
|
|
|
|
def test_bridge_reconfiguration_policy_rejects_quick_and_unresolved_physical_state(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
quick_service, _ = service_with_fake_runtime(tmp_path / "quick")
|
|
_select_connection_mode(quick_service, "quick-connect")
|
|
quick_binding = _seed_supervised_connection(
|
|
quick_service,
|
|
target_ipv4=facade_module.AP_FALLBACK_IPV4,
|
|
connection_mode="quick-connect",
|
|
transport_ref="k1-quick",
|
|
logical_device_id="device-quick",
|
|
)
|
|
quick_control = FakeInteractiveControlSession(initial_state="idle")
|
|
quick_control.open(connection_binding=quick_binding)
|
|
quick_control.verified_control = _verified_control_for_binding(
|
|
quick_binding,
|
|
logical_device_id="device-quick",
|
|
control_session_id="test-control-k1-quick",
|
|
control_proof_revision=2,
|
|
)
|
|
quick_service._application_control_session = quick_control # type: ignore[assignment] # noqa: SLF001
|
|
quick_service._reconcile_connection_supervisor( # noqa: SLF001
|
|
quick_control.snapshot(),
|
|
quick_service.runtime.snapshot(),
|
|
)
|
|
quick_service._acquire_application_control_process_lease() # noqa: SLF001
|
|
quick_state = quick_service.state()
|
|
quick_decision = quick_state["connection_policy"]["actions"]["prepare-select-device"]
|
|
assert quick_decision["allowed"] is False
|
|
assert "connection-reconfiguration-bridge-only" in quick_decision["reason_codes"]
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as quick_rejected:
|
|
asyncio.run(
|
|
quick_service.prepare_connection_reconfiguration(
|
|
_reconfiguration_request(quick_state, "select-device")
|
|
)
|
|
)
|
|
assert quick_rejected.value.reason_code == "connection-reconfiguration-bridge-only"
|
|
assert quick_control.state == "connection-ready"
|
|
|
|
bridge_service, _ = service_with_fake_runtime(tmp_path / "physical")
|
|
bridge_control, _binding, _ready = _install_authoritative_ready_bridge_control(bridge_service)
|
|
monkeypatch.setattr(
|
|
bridge_service._physical_command_coordinator, # noqa: SLF001
|
|
"snapshot",
|
|
lambda: {
|
|
"status": "unresolved",
|
|
"requires_reconciliation": True,
|
|
"record": None,
|
|
},
|
|
)
|
|
unsafe = bridge_service.state()
|
|
unsafe_decision = unsafe["connection_policy"]["actions"]["prepare-select-device"]
|
|
assert unsafe_decision["allowed"] is False
|
|
assert "physical-command-reconciliation-required" in unsafe_decision["reason_codes"]
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as physical_rejected:
|
|
asyncio.run(
|
|
bridge_service.prepare_connection_reconfiguration(
|
|
_reconfiguration_request(unsafe, "select-device")
|
|
)
|
|
)
|
|
assert physical_rejected.value.reason_code == ("physical-command-reconciliation-required")
|
|
assert bridge_control.state == "connection-ready"
|
|
assert bridge_service._connection_reconfiguration_intent is None # noqa: SLF001
|
|
|
|
|
|
def test_change_network_handoff_pins_exact_active_bridge_transport_without_write(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, _binding, ready = _install_authoritative_ready_bridge_control(service)
|
|
decision = ready["connection_policy"]["actions"]["prepare-change-network"]
|
|
assert decision["allowed"] is True
|
|
assert decision["required_transport_ref"] == "k1-a"
|
|
assert decision["required_connection_mode"] == "bridge"
|
|
device_edges: list[str] = []
|
|
|
|
async def forbidden_write(*_: object, **__: object) -> dict[str, Any]:
|
|
device_edges.append("network-write")
|
|
raise AssertionError("Prepare must remain local-only")
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_write)
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"prepare",
|
|
lambda *_args, **_kwargs: device_edges.append("physical-command"),
|
|
)
|
|
|
|
prepared = asyncio.run(
|
|
service.prepare_connection_reconfiguration(
|
|
_reconfiguration_request(ready, "change-network")
|
|
)
|
|
)
|
|
|
|
reconfiguration = prepared["connection_reconfiguration"]
|
|
assert reconfiguration["intent"] == "change-network"
|
|
assert reconfiguration["required_transport_ref"] == "k1-a"
|
|
assert reconfiguration["required_connection_mode"] == "bridge"
|
|
minimum_generation = reconfiguration["minimum_discovery_generation"]
|
|
service._ble_discovery_generation = minimum_generation # noqa: SLF001
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
observed = service.state()
|
|
assert observed["connection_reconfiguration"]["required_transport_observed"] is True
|
|
provision = observed["connection_policy"]["actions"]["provision-fresh-device"]
|
|
assert provision["required_transport_ref"] == "k1-a"
|
|
assert provision["required_connection_mode"] == "bridge"
|
|
wrong_target_request = _connect_request(
|
|
device_id="k1-b",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
connection_mode="bridge",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_mode_revision=observed["desired_connection_mode_revision"],
|
|
expected_discovery_generation=observed["ble_discovery_generation"],
|
|
expected_reconfiguration_revision=observed["connection_reconfiguration"]["revision"],
|
|
expected_reconfiguration_intent_id=observed["connection_reconfiguration"]["intent_id"],
|
|
)
|
|
password_reads: list[str] = []
|
|
|
|
def forbidden_password_read(_secret: SecretStr) -> str:
|
|
password_reads.append("unwrapped")
|
|
raise AssertionError("wrong pinned target must fail before password unwrap")
|
|
|
|
monkeypatch.setattr(SecretStr, "get_secret_value", forbidden_password_read)
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as wrong_target:
|
|
asyncio.run(service.connect(wrong_target_request))
|
|
assert wrong_target.value.reason_code == "connection-reconfiguration-target-mismatch"
|
|
assert password_reads == []
|
|
assert service._operations.snapshot() == [] # noqa: SLF001
|
|
assert control.stop_calls == 0
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
assert device_edges == []
|
|
|
|
|
|
def test_select_device_fresh_verify_consumes_exact_reconfiguration_without_write(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_durable_bridge_topology(
|
|
service,
|
|
transport_ref="k1-a",
|
|
ipv4="10.255.254.20",
|
|
)
|
|
service._pin_or_match_device_identity( # noqa: SLF001
|
|
transport_ref="k1-a",
|
|
logical_device_id="device-a",
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
)
|
|
service._pin_or_match_device_identity( # noqa: SLF001
|
|
transport_ref="k1-new",
|
|
logical_device_id="device-b",
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
)
|
|
semantic_before = service._semantic_topology_store.snapshot() # type: ignore[union-attr] # noqa: SLF001
|
|
assert semantic_before.record is not None
|
|
assert semantic_before.record.transport_ref == "k1-a"
|
|
initial = service.state()
|
|
assert initial["connection_policy"]["actions"]["prepare-select-device"]["allowed"] is True
|
|
prepared = asyncio.run(
|
|
service.prepare_connection_reconfiguration(
|
|
_reconfiguration_request(initial, "select-device")
|
|
)
|
|
)
|
|
minimum_generation = prepared["connection_reconfiguration"]["minimum_discovery_generation"]
|
|
service._ble_discovery_generation = minimum_generation # noqa: SLF001
|
|
_set_scanned_k1(service, device_id="k1-new")
|
|
fresh = service.state()
|
|
writes: list[str] = []
|
|
_install_successful_bridge_verify_transport(
|
|
monkeypatch,
|
|
device_id="k1-new",
|
|
ipv4="10.255.254.77",
|
|
writes=writes,
|
|
)
|
|
original_bootstrap = service._bootstrap_prestart_control_ready_owned # noqa: SLF001
|
|
semantic_during_bootstrap: list[str] = []
|
|
|
|
async def assert_semantic_commit_is_deferred(**kwargs: object) -> None:
|
|
before_device_info = service._semantic_topology_store.snapshot() # type: ignore[union-attr] # noqa: SLF001
|
|
assert before_device_info.record is not None
|
|
semantic_during_bootstrap.append(before_device_info.record.transport_ref)
|
|
await original_bootstrap(**kwargs) # type: ignore[arg-type]
|
|
after_device_info = service._semantic_topology_store.snapshot() # type: ignore[union-attr] # noqa: SLF001
|
|
assert after_device_info.record is not None
|
|
semantic_during_bootstrap.append(after_device_info.record.transport_ref)
|
|
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_bootstrap_prestart_control_ready_owned",
|
|
assert_semantic_commit_is_deferred,
|
|
)
|
|
|
|
verified = asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="k1-new",
|
|
source="fresh-scan",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_discovery_generation=fresh["ble_discovery_generation"],
|
|
expected_reconfiguration_revision=fresh["connection_reconfiguration"]["revision"],
|
|
expected_reconfiguration_intent_id=fresh["connection_reconfiguration"]["intent_id"],
|
|
)
|
|
)
|
|
)
|
|
|
|
assert writes == []
|
|
assert verified["selected_device_id"] == "k1-new"
|
|
assert verified["active_connection_mode"] == "bridge"
|
|
assert verified["connection_reconfiguration"]["status"] == "idle"
|
|
assert verified["connection_reconfiguration"]["intent_id"] is None
|
|
assert semantic_during_bootstrap == ["k1-a", "k1-a"]
|
|
semantic_after = service._semantic_topology_store.snapshot() # type: ignore[union-attr] # noqa: SLF001
|
|
assert semantic_after.record is not None
|
|
assert semantic_after.record.transport_ref == "k1-new"
|
|
assert semantic_after.record.revision == semantic_before.record.revision + 1
|
|
|
|
|
|
def test_select_device_identity_mismatch_preserves_a_and_cancel_restart_recovers_a(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_durable_bridge_topology(
|
|
service,
|
|
transport_ref="k1-a",
|
|
ipv4="10.255.254.20",
|
|
)
|
|
service._pin_or_match_device_identity( # noqa: SLF001
|
|
transport_ref="k1-a",
|
|
logical_device_id="device-a",
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
)
|
|
service._pin_or_match_device_identity( # noqa: SLF001
|
|
transport_ref="k1-b",
|
|
logical_device_id="device-b",
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
)
|
|
topology_store = service._semantic_topology_store # noqa: SLF001
|
|
pin_store = service._device_identity_pin_store # noqa: SLF001
|
|
assert topology_store is not None
|
|
assert pin_store is not None
|
|
semantic_before = topology_store.snapshot()
|
|
pins_before = pin_store.snapshot()
|
|
|
|
prepared = asyncio.run(
|
|
service.prepare_connection_reconfiguration(
|
|
_reconfiguration_request(service.state(), "select-device")
|
|
)
|
|
)
|
|
service._ble_discovery_generation = prepared["connection_reconfiguration"][ # noqa: SLF001
|
|
"minimum_discovery_generation"
|
|
]
|
|
_set_scanned_k1(service, device_id="k1-b")
|
|
fresh = service.state()
|
|
writes: list[str] = []
|
|
_install_successful_bridge_verify_transport(
|
|
monkeypatch,
|
|
device_id="k1-b",
|
|
ipv4="10.255.254.77",
|
|
writes=writes,
|
|
)
|
|
# Force the synthetic DeviceInfo proof to present a different logical ID
|
|
# than B's immutable transport pin. The production reducer then rejects it.
|
|
monkeypatch.setattr(service, "_expected_vendor_device_id", lambda _ref: None)
|
|
|
|
with pytest.raises(facade_module.ConnectionVerificationError) as mismatch:
|
|
asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="k1-b",
|
|
source="fresh-scan",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_discovery_generation=fresh["ble_discovery_generation"],
|
|
expected_reconfiguration_revision=fresh["connection_reconfiguration"][
|
|
"revision"
|
|
],
|
|
expected_reconfiguration_intent_id=fresh["connection_reconfiguration"][
|
|
"intent_id"
|
|
],
|
|
)
|
|
)
|
|
)
|
|
|
|
assert mismatch.value.reason_code == "control-bootstrap-device-identity-unverified"
|
|
assert writes == []
|
|
assert topology_store.snapshot() == semantic_before
|
|
assert pin_store.snapshot() == pins_before
|
|
failed = service.state()
|
|
assert failed["connection_reconfiguration"]["intent"] == "select-device"
|
|
assert failed["selected_device_id"] is None
|
|
assert failed["device_session"] is None
|
|
assert failed["connection_lifecycle"]["active_binding"] is None
|
|
assert failed["application_control_session"]["state"] == "idle"
|
|
assert [item["device_id"] for item in failed["devices"]] == ["k1-b"]
|
|
supervisor = service._connection_supervisor.snapshot() # noqa: SLF001
|
|
assert supervisor.authority.control_allowed is False
|
|
assert not (
|
|
supervisor.intent is not None
|
|
and supervisor.device_network.intent_id == supervisor.intent.intent_id
|
|
and supervisor.device_network.transport_ref == "k1-b"
|
|
)
|
|
|
|
cancelled = asyncio.run(
|
|
service.prepare_connection_reconfiguration(_reconfiguration_request(failed, "cancel"))
|
|
)
|
|
assert cancelled["connection_reconfiguration"]["status"] == "idle"
|
|
assert topology_store.snapshot() == semantic_before
|
|
assert pin_store.snapshot() == pins_before
|
|
|
|
restarted, _ = service_with_fake_runtime(tmp_path)
|
|
restarted_state = restarted.state()
|
|
restarted_record = restarted._semantic_topology_store.snapshot().record # type: ignore[union-attr] # noqa: SLF001
|
|
assert restarted_record is not None
|
|
assert restarted_record.transport_ref == "k1-a"
|
|
assert restarted_state["selected_device_id"] is None
|
|
assert restarted._configured_endpoint_target().transport_ref == "k1-a" # noqa: SLF001
|
|
|
|
|
|
def test_cold_fresh_bridge_identity_mismatch_keeps_semantic_empty_after_restart(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
service._pin_or_match_device_identity( # noqa: SLF001
|
|
transport_ref="k1-b",
|
|
logical_device_id="device-b",
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
)
|
|
topology_store = service._semantic_topology_store # noqa: SLF001
|
|
pin_store = service._device_identity_pin_store # noqa: SLF001
|
|
assert topology_store is not None
|
|
assert pin_store is not None
|
|
assert topology_store.snapshot().status == "empty"
|
|
pins_before = pin_store.snapshot()
|
|
_set_scanned_k1(service, device_id="k1-b")
|
|
fresh = service.state()
|
|
writes: list[str] = []
|
|
_install_successful_bridge_verify_transport(
|
|
monkeypatch,
|
|
device_id="k1-b",
|
|
ipv4="10.255.254.77",
|
|
writes=writes,
|
|
)
|
|
monkeypatch.setattr(service, "_expected_vendor_device_id", lambda _ref: None)
|
|
|
|
with pytest.raises(facade_module.ConnectionVerificationError) as mismatch:
|
|
asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="k1-b",
|
|
source="fresh-scan",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_discovery_generation=fresh["ble_discovery_generation"],
|
|
)
|
|
)
|
|
)
|
|
|
|
assert mismatch.value.reason_code == "control-bootstrap-device-identity-unverified"
|
|
assert writes == []
|
|
assert topology_store.snapshot().status == "empty"
|
|
assert pin_store.snapshot() == pins_before
|
|
failed = service.state()
|
|
assert failed["connection_reconfiguration"]["status"] == "idle"
|
|
assert failed["selected_device_id"] is None
|
|
assert failed["device_session"] is None
|
|
assert failed["connection_lifecycle"]["active_binding"] is None
|
|
assert failed["application_control_session"]["state"] == "idle"
|
|
assert [item["device_id"] for item in failed["devices"]] == ["k1-b"]
|
|
|
|
restarted, _ = service_with_fake_runtime(tmp_path)
|
|
restarted_state = restarted.state()
|
|
assert restarted_state["semantic_topology_store"]["status"] == "empty"
|
|
assert restarted_state["selected_device_id"] is None
|
|
|
|
|
|
def test_cold_fresh_bridge_commits_semantic_only_after_device_info_pin(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
topology_store = service._semantic_topology_store # noqa: SLF001
|
|
assert topology_store is not None
|
|
assert topology_store.snapshot().status == "empty"
|
|
_set_scanned_k1(service, device_id="k1-cold")
|
|
fresh = service.state()
|
|
writes: list[str] = []
|
|
_install_successful_bridge_verify_transport(
|
|
monkeypatch,
|
|
device_id="k1-cold",
|
|
ipv4="10.255.254.88",
|
|
writes=writes,
|
|
)
|
|
original_bootstrap = service._bootstrap_prestart_control_ready_owned # noqa: SLF001
|
|
semantic_status_during_bootstrap: list[str] = []
|
|
|
|
async def assert_cold_commit_is_deferred(**kwargs: object) -> None:
|
|
semantic_status_during_bootstrap.append(topology_store.snapshot().status)
|
|
await original_bootstrap(**kwargs) # type: ignore[arg-type]
|
|
semantic_status_during_bootstrap.append(topology_store.snapshot().status)
|
|
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_bootstrap_prestart_control_ready_owned",
|
|
assert_cold_commit_is_deferred,
|
|
)
|
|
|
|
verified = asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="k1-cold",
|
|
source="fresh-scan",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_discovery_generation=fresh["ble_discovery_generation"],
|
|
)
|
|
)
|
|
)
|
|
|
|
assert writes == []
|
|
assert semantic_status_during_bootstrap == ["empty", "empty"]
|
|
record = topology_store.snapshot().record
|
|
assert record is not None
|
|
assert record.transport_ref == "k1-cold"
|
|
assert record.ipv4 == "10.255.254.88"
|
|
assert record.revision == 1
|
|
assert verified["selected_device_id"] == "k1-cold"
|
|
assert verified["connection_reconfiguration"]["status"] == "idle"
|
|
assert verified["device_identity_pin_store"]["pin_count"] == 1
|
|
|
|
|
|
def test_select_device_cancel_during_device_info_discards_only_provisional_b(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_durable_bridge_topology(
|
|
service,
|
|
transport_ref="k1-a",
|
|
ipv4="10.255.254.20",
|
|
)
|
|
topology_store = service._semantic_topology_store # noqa: SLF001
|
|
assert topology_store is not None
|
|
semantic_before = topology_store.snapshot()
|
|
prepared = asyncio.run(
|
|
service.prepare_connection_reconfiguration(
|
|
_reconfiguration_request(service.state(), "select-device")
|
|
)
|
|
)
|
|
service._ble_discovery_generation = prepared["connection_reconfiguration"][ # noqa: SLF001
|
|
"minimum_discovery_generation"
|
|
]
|
|
_set_scanned_k1(service, device_id="k1-b")
|
|
fresh = service.state()
|
|
writes: list[str] = []
|
|
_install_successful_bridge_verify_transport(
|
|
monkeypatch,
|
|
device_id="k1-b",
|
|
ipv4="10.255.254.77",
|
|
writes=writes,
|
|
)
|
|
|
|
async def scenario() -> None:
|
|
bootstrap_entered = asyncio.Event()
|
|
|
|
async def paused_device_info(**_: object) -> None:
|
|
bootstrap_entered.set()
|
|
await asyncio.Event().wait()
|
|
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_bootstrap_prestart_control_ready_owned",
|
|
paused_device_info,
|
|
)
|
|
verify_task = asyncio.create_task(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="k1-b",
|
|
source="fresh-scan",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_discovery_generation=fresh["ble_discovery_generation"],
|
|
expected_reconfiguration_revision=fresh["connection_reconfiguration"][
|
|
"revision"
|
|
],
|
|
expected_reconfiguration_intent_id=fresh["connection_reconfiguration"][
|
|
"intent_id"
|
|
],
|
|
)
|
|
)
|
|
)
|
|
await asyncio.wait_for(bootstrap_entered.wait(), timeout=1.0)
|
|
projected = service.state()
|
|
assert projected["selected_device_id"] == "k1-b"
|
|
assert topology_store.snapshot() == semantic_before
|
|
verify_task.cancel()
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await verify_task
|
|
|
|
asyncio.run(scenario())
|
|
|
|
assert writes == []
|
|
assert topology_store.snapshot() == semantic_before
|
|
cancelled = service.state()
|
|
assert cancelled["connection_reconfiguration"]["intent"] == "select-device"
|
|
assert cancelled["selected_device_id"] is None
|
|
assert cancelled["device_session"] is None
|
|
assert cancelled["connection_lifecycle"]["active_binding"] is None
|
|
assert [item["device_id"] for item in cancelled["devices"]] == ["k1-b"]
|
|
assert cancelled["last_operation"]["status"] == "cancelled"
|
|
|
|
|
|
def test_reconfiguration_aborts_only_purely_local_prepare_without_start_or_stop(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control, _binding, _ready = _install_authoritative_ready_bridge_control(service)
|
|
control.state = "workspace-ready"
|
|
control.state_revision = 1
|
|
service._reconcile_connection_supervisor( # noqa: SLF001
|
|
control.snapshot(),
|
|
service.runtime.snapshot(),
|
|
)
|
|
prepared_acquisition = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="TEST001",
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
assert prepared_acquisition["acquisition"]["state"] == "prepared"
|
|
assert control.state == "project-ready"
|
|
device_edges: list[str] = []
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"prepare",
|
|
lambda *_args, **_kwargs: device_edges.append("physical-command"),
|
|
)
|
|
|
|
handed_off = asyncio.run(
|
|
service.prepare_connection_reconfiguration(
|
|
_reconfiguration_request(prepared_acquisition, "select-device")
|
|
)
|
|
)
|
|
|
|
assert handed_off["acquisition"]["state"] == "aborted"
|
|
assert handed_off["acquisition"]["result"] == {
|
|
"receiver_started": False,
|
|
"device_command_attempted": False,
|
|
"reason_code": "superseded-by-connection-reconfiguration",
|
|
}
|
|
assert control.start_projects == []
|
|
assert control.stop_calls == 0
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
assert device_edges == []
|
|
|
|
|
|
def test_orphaned_prestart_control_retires_on_host_epoch_loss_but_not_transient_tcp(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
control, _binding, _ready = _install_authoritative_ready_bridge_control(service)
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
before = supervisor.snapshot()
|
|
changed_path = HostPathProbeResult(
|
|
available=True,
|
|
fingerprint="test-route:changed-after-sleep",
|
|
interface="test0",
|
|
source_ipv4="192.168.1.2",
|
|
route_class="direct",
|
|
)
|
|
new_epoch = supervisor.observe_host_path(changed_path)
|
|
assert new_epoch != before.host_path.epoch
|
|
assert supervisor.snapshot().control_plane.reason_code == "host-path-epoch-changed"
|
|
|
|
retired = service.state()
|
|
|
|
assert control.state == "idle"
|
|
assert service._application_control_process_lease_holders == set() # noqa: SLF001
|
|
assert retired["connection_policy"]["actions"]["scan-ble"]["allowed"] is True
|
|
|
|
transient_service, _ = service_with_fake_runtime(tmp_path / "transient")
|
|
transient_control, _binding, _ready = _install_authoritative_ready_bridge_control(
|
|
transient_service
|
|
)
|
|
transient_supervisor = transient_service._connection_supervisor # noqa: SLF001
|
|
transient = transient_supervisor.snapshot()
|
|
assert transient.intent is not None
|
|
assert transient.device_network.target is not None
|
|
assert transient_supervisor.observe_endpoint(
|
|
target=transient.device_network.target,
|
|
intent_id=transient.intent.intent_id,
|
|
host_path_epoch=transient.host_path.epoch,
|
|
reachable=False,
|
|
reason_code="tcp-endpoint-unreachable",
|
|
)
|
|
|
|
one_negative = transient_service.state()
|
|
|
|
assert transient_control.state == "connection-ready"
|
|
assert transient_service._application_control_process_lease_holders == { # noqa: SLF001
|
|
"control"
|
|
}
|
|
assert one_negative["connection_policy"]["actions"]["scan-ble"]["allowed"] is False
|
|
|
|
|
|
def test_default_transport_observation_ttl_outlives_one_slow_healthy_monitor_cycle(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
monotonic_now = [100.0]
|
|
suspend_aware_now = [1_000.0]
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
supervisor._monotonic_clock = lambda: monotonic_now[0] # noqa: SLF001
|
|
supervisor._suspend_aware_clock = lambda: suspend_aware_now[0] # noqa: SLF001
|
|
control, _binding, _ready = _install_authoritative_ready_bridge_control(service)
|
|
|
|
# One production pass can consume 11.5 seconds plus its one-second
|
|
# interval. Observer silence through the former 15-second boundary must
|
|
# not tear down a verified pre-START owner or fabricate an epoch change.
|
|
monotonic_now[0] += 10.0
|
|
suspend_aware_now[0] += 10.0
|
|
delayed = service.state()
|
|
|
|
assert control.state == "connection-ready"
|
|
assert delayed["active_connection_mode"] == "bridge"
|
|
assert delayed["connection_lifecycle"]["active_binding_key"] is not None
|
|
assert supervisor.snapshot().authority.control_allowed is True
|
|
|
|
# The fallback now tolerates two complete bounded monitor passes.
|
|
monotonic_now[0] += 5.01
|
|
suspend_aware_now[0] += 5.01
|
|
former_boundary = service.state()
|
|
|
|
assert control.state == "connection-ready"
|
|
assert former_boundary["active_connection_mode"] == "bridge"
|
|
assert supervisor.snapshot().authority.control_allowed is True
|
|
|
|
# Complete silence beyond 30 seconds still fails closed. Existing explicit
|
|
# negative and fingerprint-change tests cover the faster loss paths.
|
|
monotonic_now[0] += 15.0
|
|
suspend_aware_now[0] += 15.0
|
|
expired = service.state()
|
|
|
|
assert control.state == "idle"
|
|
assert expired["active_connection_mode"] is None
|
|
assert supervisor.snapshot().authority.control_allowed is False
|
|
|
|
|
|
def test_orphan_retirement_waits_for_inflight_lifecycle_transition(tmp_path: Path) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
control, _binding, _ready = _install_authoritative_ready_bridge_control(service)
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
supervisor.observe_host_path(
|
|
HostPathProbeResult(
|
|
available=True,
|
|
fingerprint="test-route:bootstrap-race",
|
|
interface="test0",
|
|
source_ipv4="192.168.1.2",
|
|
route_class="direct",
|
|
)
|
|
)
|
|
assert service._k1_lifecycle_transition_gate.acquire(blocking=False) # noqa: SLF001
|
|
try:
|
|
service.state()
|
|
assert control.state == "connection-ready"
|
|
finally:
|
|
service._k1_lifecycle_transition_gate.release() # noqa: SLF001
|
|
|
|
service.state()
|
|
assert control.state == "idle"
|
|
|
|
|
|
def test_mode_select_winner_fences_connect_before_password_or_durable_admission(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
selection_entered = threading.Event()
|
|
release_selection = threading.Event()
|
|
selection_failures: list[BaseException] = []
|
|
selection_results: list[dict[str, Any]] = []
|
|
original_runtime_snapshot = runtime.snapshot
|
|
paused = False
|
|
|
|
def paused_snapshot() -> dict[str, object]:
|
|
nonlocal paused
|
|
if threading.current_thread().name == "reconfigure-mode-winner" and not paused:
|
|
paused = True
|
|
selection_entered.set()
|
|
assert release_selection.wait(2.0)
|
|
return original_runtime_snapshot()
|
|
|
|
monkeypatch.setattr(runtime, "snapshot", paused_snapshot)
|
|
|
|
def run_selection() -> None:
|
|
try:
|
|
selection_results.append(
|
|
service.select_connection_mode(
|
|
DesiredConnectionModeRequest(
|
|
connection_mode="quick-connect",
|
|
expected_revision=0,
|
|
)
|
|
)
|
|
)
|
|
except BaseException as exc: # pragma: no cover - assertion aid
|
|
selection_failures.append(exc)
|
|
|
|
selection_thread = threading.Thread(
|
|
target=run_selection,
|
|
name="reconfigure-mode-winner",
|
|
daemon=True,
|
|
)
|
|
selection_thread.start()
|
|
assert selection_entered.wait(2.0)
|
|
password_reads: list[str] = []
|
|
losing_request = _connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
connection_mode="bridge",
|
|
expected_mode_revision=0,
|
|
expected_discovery_generation=0,
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
|
|
def forbidden_password_read(_secret: SecretStr) -> str:
|
|
password_reads.append("unwrapped")
|
|
raise AssertionError("losing Connect must fail before password unwrap")
|
|
|
|
monkeypatch.setattr(SecretStr, "get_secret_value", forbidden_password_read)
|
|
try:
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as losing_connect:
|
|
asyncio.run(service.connect(losing_request))
|
|
finally:
|
|
release_selection.set()
|
|
selection_thread.join(timeout=3.0)
|
|
|
|
assert losing_connect.value.reason_code == "connection-reconfiguration-lifecycle-busy"
|
|
assert password_reads == []
|
|
assert selection_thread.is_alive() is False
|
|
assert selection_failures == []
|
|
assert selection_results[0]["desired_connection_mode"] == "quick-connect"
|
|
assert service._operations.snapshot() == [] # noqa: SLF001
|
|
assert service._network_provisioning_idempotency_journal.snapshot().records == () # noqa: SLF001
|
|
|
|
|
|
def test_bridge_host_association_is_one_shot_and_connection_finishes_ready(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
writes = 0
|
|
associations: list[tuple[str, str]] = []
|
|
route_classes = iter(["default-route", "direct-or-routed"])
|
|
|
|
async def successful_write(
|
|
*_: object,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**__: object,
|
|
) -> dict[str, Any]:
|
|
nonlocal writes
|
|
writes += 1
|
|
_dispatch_test_network_write(on_write_dispatch)
|
|
return {
|
|
"started_at_utc": "2026-08-09T10:00:00Z",
|
|
"completed_at_utc": "2026-08-09T10:00:01Z",
|
|
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
|
"outcome": "lan_address_observed",
|
|
"write_mode": "with_response",
|
|
"observations": [
|
|
{"status": _wifi_status_read("192.168.68.50", device_id="k1-a")["status"]}
|
|
],
|
|
}
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", successful_write)
|
|
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
|
|
monkeypatch.setattr(facade_module, "_host_route_class", lambda _target: next(route_classes))
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"associate_with_ephemeral_wifi_credentials_once",
|
|
lambda _helper, ssid, password, **_kwargs: (
|
|
associations.append((ssid, password))
|
|
or {
|
|
"adapter": "CoreWLAN",
|
|
"outcome": "associated",
|
|
"already_associated": False,
|
|
"scan_attempt_count": 1,
|
|
"scan_elapsed_ms": 25,
|
|
}
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
_reachable_control_endpoint,
|
|
)
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", lambda _target: True)
|
|
|
|
state = asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
allow_host_wifi_switch=True,
|
|
)
|
|
)
|
|
)
|
|
|
|
assert writes == 1
|
|
assert associations == [("lab-router", PRIMARY_TEST_CREDENTIAL)]
|
|
assert state["connection_lifecycle"]["connection_ready"] is True
|
|
assert state["connection_lifecycle"]["active_mode"] == "bridge"
|
|
operation = next(item for item in state["operations"] if item["action"] == "network.provision")
|
|
assert operation["result"]["host_route_class"] is None
|
|
assert operation["result"]["control_endpoint_reachable"] is None
|
|
assert operation["result"]["host_wifi_switch_authorized"] is True
|
|
assert operation["result"]["host_wifi_association_performed"] is True
|
|
assert PRIMARY_TEST_CREDENTIAL not in str(state)
|
|
|
|
|
|
def test_bridge_host_wifi_switch_defaults_to_denied_and_does_not_replay_network_write(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
writes = 0
|
|
association_calls: list[str] = []
|
|
|
|
async def successful_write(
|
|
*_: object,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**__: object,
|
|
) -> dict[str, Any]:
|
|
nonlocal writes
|
|
writes += 1
|
|
_dispatch_test_network_write(on_write_dispatch)
|
|
return {
|
|
"started_at_utc": "2026-08-09T10:30:00Z",
|
|
"completed_at_utc": "2026-08-09T10:30:01Z",
|
|
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
|
"outcome": "lan_address_observed",
|
|
"write_mode": "with_response",
|
|
"observations": [
|
|
{"status": _wifi_status_read("192.168.68.50", device_id="k1-a")["status"]}
|
|
],
|
|
}
|
|
|
|
def forbidden_host_association(*_: object, **__: object) -> dict[str, Any]:
|
|
association_calls.append("attempted")
|
|
raise AssertionError("Bridge must not switch host Wi-Fi without explicit opt-in")
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", successful_write)
|
|
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
|
|
monkeypatch.setattr(facade_module, "_host_route_class", lambda _target: "tunnel")
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _tunnel_host_path)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"associate_with_ephemeral_wifi_credentials_once",
|
|
forbidden_host_association,
|
|
)
|
|
request = _connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
idempotency_key="bridge-host-switch-default-deny",
|
|
)
|
|
assert request.allow_host_wifi_switch is False
|
|
|
|
first = asyncio.run(service.connect(request))
|
|
replay = asyncio.run(service.connect(request))
|
|
|
|
assert writes == 1
|
|
assert association_calls == []
|
|
operation = next(item for item in first["operations"] if item["action"] == "network.provision")
|
|
assert operation["status"] == "succeeded"
|
|
assert operation["result"]["phase"] == "network_applied"
|
|
assert operation["result"]["host_wifi_switch_authorized"] is False
|
|
assert operation["result"]["host_wifi_association_performed"] is False
|
|
assert operation["result"]["host_wifi_association_outcome"] == "not-authorized"
|
|
assert first["connection_attempt"]["phase"] == "network_applied"
|
|
assert first["connection_attempt"]["control_state"] == "unknown"
|
|
assert first["connection_attempt"]["safe_next_action"] == "wait-for-current-attempt"
|
|
assert replay["connection_attempt"]["phase"] == "network_applied"
|
|
assert replay["connection_attempt"]["control_state"] == "unknown"
|
|
assert replay["connection_attempt"]["safe_next_action"] == "verify-control-read-only"
|
|
assert PRIMARY_TEST_CREDENTIAL not in str(first)
|
|
|
|
|
|
def test_applied_network_bootstrap_failure_recovers_by_verify_without_rewrite(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
writes = 0
|
|
bootstrap_attempts = 0
|
|
|
|
async def successful_write(
|
|
*_: object,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**__: object,
|
|
) -> dict[str, Any]:
|
|
nonlocal writes
|
|
writes += 1
|
|
_dispatch_test_network_write(on_write_dispatch)
|
|
return {
|
|
"started_at_utc": "2026-08-09T11:00:00Z",
|
|
"completed_at_utc": "2026-08-09T11:00:01Z",
|
|
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
|
"outcome": "lan_address_observed",
|
|
"write_mode": "with_response",
|
|
"observations": [
|
|
{"status": _wifi_status_read("192.168.68.50", device_id="k1-a")["status"]}
|
|
],
|
|
}
|
|
|
|
async def failed_bootstrap(
|
|
bound_service: XgridsK1CompatibilityService,
|
|
*,
|
|
parent_operation_id: str | None,
|
|
connection_mode: facade_module.ConnectionMode,
|
|
inspection_only: bool = False,
|
|
) -> None:
|
|
nonlocal bootstrap_attempts
|
|
assert inspection_only is True
|
|
bootstrap_attempts += 1
|
|
operation, _ = bound_service._operations.begin( # noqa: SLF001
|
|
facade_module.ACTION_CONNECTION_CONTROL_BOOTSTRAP,
|
|
context={
|
|
"connection_mode": connection_mode,
|
|
"parent_operation_id": parent_operation_id,
|
|
"automatic_retry": False,
|
|
},
|
|
)
|
|
bound_service._operations.transition( # noqa: SLF001
|
|
operation.operation_id,
|
|
"running",
|
|
stage_code="mqtt-device-info",
|
|
message_code="connection.control_bootstrap.running",
|
|
)
|
|
failure = facade_module.ConnectionVerificationError(
|
|
"synthetic DeviceInfo timeout",
|
|
reason_code="control-bootstrap-timeout",
|
|
)
|
|
bound_service._operations.transition( # noqa: SLF001
|
|
operation.operation_id,
|
|
"failed",
|
|
stage_code="device-info-failed",
|
|
message_code="connection.control_bootstrap.failed",
|
|
error={
|
|
"category": "connection",
|
|
"code": failure.reason_code,
|
|
"retryable": True,
|
|
"safe_to_retry": True,
|
|
"side_effect_status": "none",
|
|
},
|
|
)
|
|
raise failure
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", successful_write)
|
|
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, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
_reachable_control_endpoint,
|
|
)
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", lambda _target: True)
|
|
service._bootstrap_prestart_control_ready_owned = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
failed_bootstrap,
|
|
service,
|
|
)
|
|
request = _connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
idempotency_key="applied-network-device-info-recovery",
|
|
)
|
|
|
|
failed = asyncio.run(service.connect(request))
|
|
assert writes == 1
|
|
assert bootstrap_attempts == 1
|
|
assert failed["configured_connection_mode"] == "bridge"
|
|
assert failed["active_connection_mode"] is None
|
|
assert failed["connection_lifecycle"]["connection_ready"] is False
|
|
assert failed["connection_attempt"]["status"] == "failed"
|
|
assert failed["connection_attempt"]["phase"] == "network_applied"
|
|
assert failed["connection_attempt"]["control_state"] == "control_not_ready"
|
|
assert failed["connection_attempt"]["safe_next_action"] == "verify-control-read-only"
|
|
assert failed["network_mutation_ledger"]["resolution"] == "target-observed"
|
|
assert failed["network_provisioning_idempotency"]["active_operation_id"] is None
|
|
|
|
replayed = asyncio.run(service.connect(request))
|
|
assert writes == 1
|
|
assert bootstrap_attempts == 1
|
|
assert replayed["connection_lifecycle"]["connection_ready"] is False
|
|
assert replayed["connection_attempt"]["phase"] == "network_applied"
|
|
assert replayed["connection_attempt"]["control_state"] == "control_not_ready"
|
|
|
|
service._bootstrap_prestart_control_ready_owned = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
_synthetic_prestart_control_bootstrap,
|
|
service,
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_probe_control_endpoint_socket",
|
|
lambda _target: facade_module.TcpReachabilityProbeResult(reachable=True),
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"read_wifi_status_once",
|
|
lambda *_args, **_kwargs: asyncio.sleep(
|
|
0,
|
|
result=_wifi_status_read("192.168.68.50", device_id="k1-a"),
|
|
),
|
|
)
|
|
recovered = asyncio.run(service.verify_connection(ConnectionVerifyRequest()))
|
|
|
|
assert writes == 1
|
|
assert recovered["connection_lifecycle"]["connection_ready"] is True
|
|
assert recovered["connection_lifecycle"]["active_mode"] == "bridge"
|
|
assert recovered["last_operation"]["action"] == "connection.verify"
|
|
assert recovered["last_operation"]["status"] == "succeeded"
|
|
assert recovered["connection_attempt"]["status"] == "succeeded"
|
|
assert recovered["connection_attempt"]["phase"] == "network_applied"
|
|
assert recovered["connection_attempt"]["control_state"] == "ready"
|
|
assert recovered["connection_attempt"]["safe_next_action"] == "start-acquisition"
|
|
assert (
|
|
recovered["connection_attempt"]["recovery_operation_id"]
|
|
== recovered["last_operation"]["operation_id"]
|
|
)
|
|
|
|
semantic_after_verify = service._semantic_topology_store.snapshot() # type: ignore[union-attr] # noqa: SLF001
|
|
assert semantic_after_verify.record is not None
|
|
assert semantic_after_verify.record.source == "ble-read-only-status"
|
|
|
|
restarted_service, _ = service_with_fake_runtime(tmp_path)
|
|
restarted_replay = asyncio.run(restarted_service.connect(request))
|
|
restarted_network = next(
|
|
item
|
|
for item in restarted_replay["operations"]
|
|
if item["action"] == facade_module.ACTION_NETWORK_PROVISION
|
|
)
|
|
assert writes == 1
|
|
assert restarted_network["stage_code"] == "durable-terminal-replay"
|
|
assert restarted_network["result"]["replay_binding_available"] is True
|
|
assert restarted_network["result"]["target_ipv4"] == "192.168.68.50"
|
|
assert restarted_replay["connection_attempt"]["control_state"] == "unknown"
|
|
|
|
# A same-UUID advertisement is incidental presence, not a reason to hide
|
|
# the exact resolved-Apply LAN recovery target or force another GATT read.
|
|
_set_scanned_k1(restarted_service, device_id="k1-a")
|
|
advertised_recovery = restarted_service.state()
|
|
configured_observation = advertised_recovery["connection_policy"]["actions"][
|
|
"observe-configured-device-network"
|
|
]
|
|
assert advertised_recovery["current_device_recovery"] is None
|
|
assert configured_observation["allowed"] is True
|
|
assert configured_observation["required_transport_ref"] == "k1-a"
|
|
assert configured_observation["required_connection_mode"] == "bridge"
|
|
assert configured_observation["requires_live_gatt_validation"] is False
|
|
assert (
|
|
"fresh-candidate-supersedes-durable-recovery" not in configured_observation["reason_codes"]
|
|
)
|
|
|
|
durable_ble_reads = 0
|
|
|
|
async def forbidden_durable_ble_read(*_: object, **__: object) -> dict[str, Any]:
|
|
nonlocal durable_ble_reads
|
|
durable_ble_reads += 1
|
|
raise AssertionError("resolved Apply recovery must stay LAN/MQTT-only")
|
|
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"read_wifi_status_once",
|
|
forbidden_durable_ble_read,
|
|
)
|
|
|
|
restarted_verified = asyncio.run(
|
|
restarted_service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="k1-a",
|
|
source="durable-configured-state",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
assert writes == 1
|
|
assert durable_ble_reads == 0
|
|
assert restarted_verified["last_operation"]["action"] == "connection.verify"
|
|
assert restarted_verified["last_operation"]["status"] == "succeeded"
|
|
assert (
|
|
restarted_verified["connection_attempt"]["attempt_id"] == restarted_network["operation_id"]
|
|
)
|
|
assert restarted_verified["connection_attempt"]["control_state"] == "ready"
|
|
assert restarted_verified["connection_attempt"]["safe_next_action"] == ("start-acquisition")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("mismatched_field", "mismatched_value"),
|
|
[
|
|
("transport_ref", "foreign-k1"),
|
|
("connection_mode", "quick-connect"),
|
|
("ipv4", "192.168.68.99"),
|
|
("source", "foreign-evidence-source"),
|
|
("compatibility_profile_id", "foreign.compatibility.profile"),
|
|
],
|
|
)
|
|
def test_resolved_apply_durable_mismatch_blocks_policy_and_verify_without_io(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
mismatched_field: str,
|
|
mismatched_value: str,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
ledger = service._network_mutation_ledger # noqa: SLF001
|
|
observation = NetworkStatusEvidence(
|
|
mode="WIFI_CLIENT",
|
|
ipv4="192.168.68.50",
|
|
status_code=1,
|
|
reserved=0,
|
|
)
|
|
prepared = ledger.prepare(
|
|
operation_id="resolved-apply-mismatch",
|
|
transport_ref="k1-a",
|
|
intended_mode="bridge",
|
|
write_mode="with_response",
|
|
baseline_status=NetworkStatusEvidence(
|
|
mode="WIFI_AP",
|
|
ipv4="192.168.56.1",
|
|
status_code=1,
|
|
reserved=1,
|
|
),
|
|
)
|
|
dispatching = ledger.mark_dispatching(
|
|
prepared.operation_id,
|
|
expected_revision=prepared.revision,
|
|
)
|
|
observing = ledger.mark_observing(
|
|
dispatching.operation_id,
|
|
expected_revision=dispatching.revision,
|
|
write_confirmed=True,
|
|
observation=observation,
|
|
)
|
|
ledger.resolve(
|
|
observing.operation_id,
|
|
expected_revision=observing.revision,
|
|
resolution="target-observed",
|
|
)
|
|
|
|
topology_store = service._semantic_topology_store # noqa: SLF001
|
|
assert topology_store is not None
|
|
exact_record = topology_store.commit(
|
|
transport_ref="k1-a",
|
|
connection_mode="bridge",
|
|
ipv4="192.168.68.50",
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
firmware_version=ATTESTATION.firmware_version,
|
|
source="ble-post-write-status",
|
|
observed_at_utc="2026-08-11T12:00:00Z",
|
|
)
|
|
mismatched_record = exact_record.as_dict()
|
|
mismatched_record[mismatched_field] = mismatched_value
|
|
mismatched_snapshot_dict = {
|
|
"schema_version": "missioncore.xgrids-k1-semantic-topology/v1",
|
|
"status": "available",
|
|
"configured_offline_evidence": True,
|
|
"live_connection_authority": False,
|
|
"reason_code": None,
|
|
"record": mismatched_record,
|
|
}
|
|
mismatched_snapshot = SimpleNamespace(
|
|
status="available",
|
|
record=SimpleNamespace(**mismatched_record),
|
|
reason_code=None,
|
|
configured_offline_evidence=True,
|
|
live_connection_authority=False,
|
|
as_dict=lambda: mismatched_snapshot_dict,
|
|
)
|
|
monkeypatch.setattr(topology_store, "snapshot", lambda: mismatched_snapshot)
|
|
|
|
calls = {"host_probe": 0, "ble_read": 0, "network_write": 0, "control_open": 0}
|
|
|
|
def forbidden_host_probe(*_: object, **__: object) -> None:
|
|
calls["host_probe"] += 1
|
|
raise AssertionError("mismatched durable target must not probe the host")
|
|
|
|
async def forbidden_ble_read(*_: object, **__: object) -> dict[str, Any]:
|
|
calls["ble_read"] += 1
|
|
raise AssertionError("mismatched durable target must not read BLE")
|
|
|
|
async def forbidden_network_write(*_: object, **__: object) -> dict[str, Any]:
|
|
calls["network_write"] += 1
|
|
raise AssertionError("mismatched durable target must not write the network")
|
|
|
|
def forbidden_control_open(*_: object, **__: object) -> dict[str, object]:
|
|
calls["control_open"] += 1
|
|
raise AssertionError("mismatched durable target must not open control")
|
|
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_probe_configured_endpoint_host_only",
|
|
forbidden_host_probe,
|
|
)
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_ble_read)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_network_write)
|
|
monkeypatch.setattr(
|
|
service._application_control_session, # noqa: SLF001
|
|
"open",
|
|
forbidden_control_open,
|
|
)
|
|
|
|
before = service.state()
|
|
decision = before["connection_policy"]["actions"]["observe-configured-device-network"]
|
|
assert decision["allowed"] is False
|
|
assert "resolved-apply-durable-target-mismatch" in decision["reason_codes"]
|
|
assert decision["required_transport_ref"] is None
|
|
assert decision.get("required_connection_mode") is None
|
|
|
|
with pytest.raises(facade_module.ConnectionVerificationError) as raised:
|
|
asyncio.run(
|
|
service.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="k1-a",
|
|
source="durable-configured-state",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
assert raised.value.reason_code == ("connection-verify-resolved-apply-target-mismatch")
|
|
assert calls == {
|
|
"host_probe": 0,
|
|
"ble_read": 0,
|
|
"network_write": 0,
|
|
"control_open": 0,
|
|
}
|
|
after = ledger.snapshot()
|
|
assert after.record is not None
|
|
assert after.record.operation_id == "resolved-apply-mismatch"
|
|
assert after.record.write_confirmed is True
|
|
assert after.record.resolution == "target-observed"
|
|
|
|
|
|
def test_applied_network_bootstrap_cancellation_does_not_cancel_fast_ack_or_rewrite(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
writes = 0
|
|
|
|
async def successful_write(
|
|
*_: object,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**__: object,
|
|
) -> dict[str, Any]:
|
|
nonlocal writes
|
|
writes += 1
|
|
_dispatch_test_network_write(on_write_dispatch)
|
|
return {
|
|
"started_at_utc": "2026-08-09T11:30:00Z",
|
|
"completed_at_utc": "2026-08-09T11:30:01Z",
|
|
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
|
"outcome": "lan_address_observed",
|
|
"write_mode": "with_response",
|
|
"observations": [
|
|
{"status": _wifi_status_read("192.168.68.50", device_id="k1-a")["status"]}
|
|
],
|
|
}
|
|
|
|
def cancelled_control_open(**_: object) -> None:
|
|
raise asyncio.CancelledError
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", successful_write)
|
|
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, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
_reachable_control_endpoint,
|
|
)
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", lambda _target: True)
|
|
service._bootstrap_prestart_control_ready_owned = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
XgridsK1CompatibilityService._bootstrap_prestart_control_ready_owned, # noqa: SLF001
|
|
service,
|
|
)
|
|
monkeypatch.setattr(
|
|
service._application_control_session, # noqa: SLF001
|
|
"open",
|
|
cancelled_control_open,
|
|
)
|
|
request = _connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
idempotency_key="applied-network-device-info-cancelled",
|
|
)
|
|
|
|
fast_ack = asyncio.run(service.connect(request))
|
|
assert fast_ack["connection_attempt"]["phase"] == "network_applied"
|
|
|
|
cancelled = service.state()
|
|
assert writes == 1
|
|
network_operation = next(
|
|
item for item in cancelled["operations"] if item["action"] == "network.provision"
|
|
)
|
|
assert network_operation["status"] == "succeeded"
|
|
assert network_operation["result"]["phase"] == "network_applied"
|
|
bootstrap_operation = next(
|
|
item
|
|
for item in service._operations.snapshot() # noqa: SLF001
|
|
if item["action"] == facade_module.ACTION_CONNECTION_CONTROL_BOOTSTRAP
|
|
)
|
|
assert bootstrap_operation["status"] == "cancelled"
|
|
assert cancelled["connection_attempt"]["phase"] == "network_applied"
|
|
assert cancelled["connection_attempt"]["control_state"] == "unknown"
|
|
assert cancelled["connection_attempt"]["safe_next_action"] == ("verify-control-read-only")
|
|
|
|
replayed = asyncio.run(service.connect(request))
|
|
assert writes == 1
|
|
assert replayed["connection_attempt"]["phase"] == "network_applied"
|
|
assert replayed["connection_attempt"]["control_state"] == "unknown"
|
|
|
|
|
|
def _install_successful_fast_ack_bridge_write(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> list[str]:
|
|
writes: list[str] = []
|
|
|
|
async def successful_write(
|
|
*_: object,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**__: object,
|
|
) -> dict[str, Any]:
|
|
writes.append("network-write")
|
|
_dispatch_test_network_write(on_write_dispatch)
|
|
return {
|
|
"started_at_utc": "2026-08-11T10:00:00Z",
|
|
"completed_at_utc": "2026-08-11T10:00:01Z",
|
|
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
|
"outcome": "lan_address_observed",
|
|
"write_mode": "with_response",
|
|
"observations": [
|
|
{"status": _wifi_status_read("192.168.68.50", device_id="k1-a")["status"]}
|
|
],
|
|
}
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", successful_write)
|
|
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", lambda _target: True)
|
|
return writes
|
|
|
|
|
|
def test_connect_fast_ack_does_not_wait_for_blocked_endpoint_and_write_is_once(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
writes = _install_successful_fast_ack_bridge_write(monkeypatch)
|
|
request = _connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
idempotency_key="fast-ack-blocked-endpoint-once",
|
|
)
|
|
|
|
async def scenario() -> None:
|
|
endpoint_started = asyncio.Event()
|
|
release_endpoint = asyncio.Event()
|
|
|
|
async def blocked_endpoint(*_: object, **__: object) -> object:
|
|
endpoint_started.set()
|
|
await release_endpoint.wait()
|
|
path = _direct_host_path("192.168.68.50")
|
|
service._observe_connection_transport( # noqa: SLF001
|
|
"192.168.68.50",
|
|
path=path,
|
|
reachable=True,
|
|
)
|
|
return facade_module._CorrelatedEndpointObservation( # noqa: SLF001
|
|
path=path,
|
|
reachable=True,
|
|
reason_code=None,
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
blocked_endpoint,
|
|
)
|
|
fast_ack = await asyncio.wait_for(service.connect(request), timeout=1.0)
|
|
assert endpoint_started.is_set()
|
|
assert writes == ["network-write"]
|
|
network_operation = next(
|
|
item
|
|
for item in fast_ack["operations"]
|
|
if item["action"] == facade_module.ACTION_NETWORK_PROVISION
|
|
)
|
|
assert network_operation["status"] == "succeeded"
|
|
assert network_operation["stage_code"] == "network-configured"
|
|
assert network_operation["result"]["phase"] == "network_applied"
|
|
assert network_operation["result"]["control_state"] == "unknown"
|
|
assert network_operation["result"]["host_route_ready"] is None
|
|
assert network_operation["result"]["control_endpoint_reachable"] is None
|
|
assert fast_ack["connection_attempt"]["control_state"] == "unknown"
|
|
assert fast_ack["connection_attempt"]["safe_next_action"] == ("wait-for-current-attempt")
|
|
|
|
with service._lock: # noqa: SLF001
|
|
continuation_task = service._control_bootstrap_continuation_task # noqa: SLF001
|
|
assert continuation_task is not None
|
|
assert not continuation_task.done()
|
|
release_endpoint.set()
|
|
await asyncio.wait_for(asyncio.shield(continuation_task), timeout=1.0)
|
|
ready = service.state()
|
|
assert ready["connection_attempt"]["control_state"] == "ready"
|
|
assert ready["connection_attempt"]["safe_next_action"] == "start-acquisition"
|
|
|
|
replay = await service.connect(request)
|
|
assert writes == ["network-write"]
|
|
assert replay["connection_attempt"]["phase"] == "network_applied"
|
|
|
|
asyncio.run(scenario())
|
|
|
|
|
|
def test_connect_fast_ack_does_not_wait_for_blocked_device_info(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
writes = _install_successful_fast_ack_bridge_write(monkeypatch)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
_reachable_control_endpoint,
|
|
)
|
|
synthetic_bootstrap = service._bootstrap_prestart_control_ready_owned # noqa: SLF001
|
|
request = _connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
idempotency_key="fast-ack-blocked-device-info",
|
|
)
|
|
|
|
async def scenario() -> None:
|
|
device_info_started = asyncio.Event()
|
|
release_device_info = asyncio.Event()
|
|
|
|
async def blocked_bootstrap(
|
|
_service: XgridsK1CompatibilityService,
|
|
*,
|
|
parent_operation_id: str | None,
|
|
connection_mode: facade_module.ConnectionMode,
|
|
inspection_only: bool = False,
|
|
) -> None:
|
|
device_info_started.set()
|
|
await release_device_info.wait()
|
|
await synthetic_bootstrap(
|
|
parent_operation_id=parent_operation_id,
|
|
connection_mode=connection_mode,
|
|
inspection_only=inspection_only,
|
|
)
|
|
|
|
service._bootstrap_prestart_control_ready_owned = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
blocked_bootstrap,
|
|
service,
|
|
)
|
|
fast_ack = await asyncio.wait_for(service.connect(request), timeout=1.0)
|
|
assert device_info_started.is_set()
|
|
assert writes == ["network-write"]
|
|
assert fast_ack["connection_attempt"]["status"] == "running"
|
|
assert fast_ack["connection_attempt"]["control_state"] == "unknown"
|
|
assert fast_ack["connection_attempt"]["safe_next_action"] == ("wait-for-current-attempt")
|
|
with service._lock: # noqa: SLF001
|
|
continuation_task = service._control_bootstrap_continuation_task # noqa: SLF001
|
|
assert continuation_task is not None
|
|
release_device_info.set()
|
|
await asyncio.wait_for(asyncio.shield(continuation_task), timeout=1.0)
|
|
assert service.state()["connection_attempt"]["control_state"] == "ready"
|
|
|
|
asyncio.run(scenario())
|
|
|
|
|
|
@pytest.mark.parametrize("observed_state", ["ready", "scanning"])
|
|
def test_reset_retired_apply_continuation_settles_without_receiver_rehydrate(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
observed_state: str,
|
|
) -> None:
|
|
"""A reset-owned Apply settles physical truth without reviving capture."""
|
|
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
old_binding = _seed_supervised_connection(
|
|
service,
|
|
transport_ref="k1-a",
|
|
logical_device_id="k1-a",
|
|
)
|
|
_seed_unresolved_physical_stop_for_retirement(service, old_binding)
|
|
reset = service.select_connection_mode(
|
|
DesiredConnectionModeRequest(
|
|
connection_mode="bridge",
|
|
expected_revision=service.state()["desired_connection_mode_revision"],
|
|
reset_scenario=True,
|
|
reset_id="reset-retired-apply-scanning-stop-only-0001",
|
|
)
|
|
)
|
|
|
|
async def fresh_scan(
|
|
_duration_seconds: float,
|
|
*,
|
|
on_admitted: Callable[[], None] | None = None,
|
|
) -> dict[str, Any]:
|
|
assert on_admitted is not None
|
|
on_admitted()
|
|
return _ble_scan_result("k1-a")
|
|
|
|
monkeypatch.setattr(facade_module, "scan", fresh_scan)
|
|
scanned = asyncio.run(service.scan_ble(BleScanRequest(duration_seconds=1.0)))
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
writes = _install_successful_fast_ack_bridge_write(monkeypatch)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
_reachable_control_endpoint,
|
|
)
|
|
synthetic_bootstrap = service._bootstrap_prestart_control_ready_owned # noqa: SLF001
|
|
coordinator = service._physical_command_coordinator # noqa: SLF001
|
|
inspection_modes: list[bool] = []
|
|
|
|
async def physical_bootstrap(
|
|
bound_service: XgridsK1CompatibilityService,
|
|
*,
|
|
parent_operation_id: str | None,
|
|
connection_mode: facade_module.ConnectionMode,
|
|
inspection_only: bool = False,
|
|
) -> None:
|
|
inspection_modes.append(inspection_only)
|
|
assert inspection_only is True
|
|
coordinator.prepare_read_only_bootstrap()
|
|
coordinator.application_response(
|
|
ApplicationMqttResponseEvidence(
|
|
operation_key=f"bootstrap:{parent_operation_id}:DeviceInfoRequest",
|
|
response_topic="lixel/application/response/device_info",
|
|
payload_sha256="7" * 64,
|
|
modeling_action=None,
|
|
result_code=None,
|
|
success=None,
|
|
observed_at_utc="2026-08-14T08:30:00.000Z",
|
|
)
|
|
)
|
|
await synthetic_bootstrap(
|
|
parent_operation_id=parent_operation_id,
|
|
connection_mode=connection_mode,
|
|
inspection_only=inspection_only,
|
|
)
|
|
verified = bound_service._application_control_session.snapshot().get( # noqa: SLF001
|
|
"verified_control"
|
|
)
|
|
assert isinstance(verified, dict)
|
|
reopened = bound_service._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
assert reopened is not None
|
|
assert reopened.stage == "observing"
|
|
coordinator.bind_control_session(
|
|
PhysicalCommandRuntimeBinding(
|
|
vendor_device_id_sha256=(
|
|
reopened.identity.vendor_device_id_sha256
|
|
),
|
|
device_serial_sha256=reopened.identity.device_serial_sha256,
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
intent_id=str(verified["intent_id"]),
|
|
transport_ref=str(verified["transport_ref"]),
|
|
connection_mode=verified["connection_mode"], # type: ignore[arg-type]
|
|
target_ipv4=str(verified["target_ipv4"]),
|
|
target_port=int(verified["target_port"]),
|
|
host_path_epoch=int(verified["host_path_epoch"]),
|
|
control_session_id=str(verified["control_session_id"]),
|
|
producer_generation=int(verified["producer_generation"]),
|
|
)
|
|
)
|
|
coordinator.device_status(
|
|
ApplicationMqttDeviceStatusEvidence(
|
|
vendor_device_id_sha256=(
|
|
reopened.identity.vendor_device_id_sha256
|
|
),
|
|
device_serial_sha256=reopened.identity.device_serial_sha256,
|
|
session_state=observed_state, # type: ignore[arg-type]
|
|
session_state_code=(
|
|
MODELING_STATE_BASE + (302 if observed_state == "scanning" else 300)
|
|
),
|
|
project_bound=observed_state == "scanning",
|
|
project_id_sha256=(
|
|
"8" * 64 if observed_state == "scanning" else None
|
|
),
|
|
init_ready=observed_state == "scanning",
|
|
status_message_sha256="9" * 64,
|
|
mqtt_retained=False,
|
|
observed_at_utc="2026-08-14T08:30:01.000Z",
|
|
)
|
|
)
|
|
bound_service._acquire_application_control_process_lease() # noqa: SLF001
|
|
|
|
async def forbidden_receiver_rehydrate(**_kwargs: object) -> None:
|
|
pytest.fail("reset-owned SCANNING settlement must not recreate a receiver")
|
|
|
|
service._bootstrap_prestart_control_ready_owned = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
physical_bootstrap,
|
|
service,
|
|
)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_rehydrate_active_acquisition_after_restart",
|
|
forbidden_receiver_rehydrate,
|
|
)
|
|
request = _connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
idempotency_key="reset-retired-apply-scanning-stop-only",
|
|
operation_id="op-00000000-0000-4000-8000-000000001401",
|
|
expected_mode_revision=reset["desired_connection_mode_revision"],
|
|
expected_discovery_generation=scanned["ble_discovery_generation"],
|
|
)
|
|
|
|
async def scenario() -> tuple[
|
|
dict[str, Any],
|
|
dict[str, Any],
|
|
dict[str, Any] | None,
|
|
]:
|
|
applied = await service.connect(request)
|
|
with service._lock: # noqa: SLF001
|
|
continuation_task = service._control_bootstrap_continuation_task # noqa: SLF001
|
|
if continuation_task is not None:
|
|
await asyncio.wait_for(asyncio.shield(continuation_task), timeout=1.0)
|
|
settled = service.state()
|
|
await service.connect(request.model_copy(update={"operation_id": None}))
|
|
successor: dict[str, Any] | None = None
|
|
if observed_state == "ready":
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as stale:
|
|
await service.connect(
|
|
request.model_copy(
|
|
update={
|
|
"operation_id": None,
|
|
"ssid": "changed-without-reconfiguration",
|
|
}
|
|
)
|
|
)
|
|
assert stale.value.reason_code == "physical-command-target-retired"
|
|
prepared = await service.prepare_connection_reconfiguration(
|
|
_reconfiguration_request(settled, "change-network")
|
|
)
|
|
fresh = await service.scan_ble(BleScanRequest(duration_seconds=1.0))
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_schedule_control_bootstrap_continuation",
|
|
lambda **_kwargs: None,
|
|
)
|
|
successor = await service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="new-lab-router",
|
|
password=SecretStr(SECONDARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
idempotency_key="reset-ready-explicit-change-network-successor",
|
|
operation_id="op-00000000-0000-4000-8000-000000001402",
|
|
expected_mode_revision=prepared[
|
|
"desired_connection_mode_revision"
|
|
],
|
|
expected_discovery_generation=fresh[
|
|
"ble_discovery_generation"
|
|
],
|
|
expected_reconfiguration_revision=fresh[
|
|
"connection_reconfiguration"
|
|
]["revision"],
|
|
expected_reconfiguration_intent_id=fresh[
|
|
"connection_reconfiguration"
|
|
]["intent_id"],
|
|
)
|
|
)
|
|
return applied, settled, successor
|
|
|
|
applied, settled, successor = asyncio.run(scenario())
|
|
physical = settled["physical_command"]
|
|
control = settled["application_control_session"]
|
|
acquisition = settled["acquisition"]
|
|
|
|
assert applied["connection_attempt"]["phase"] == "network_applied"
|
|
assert inspection_modes == [True]
|
|
assert writes == (
|
|
["network-write", "network-write"]
|
|
if observed_state == "ready"
|
|
else ["network-write"]
|
|
)
|
|
expected_resolution = (
|
|
"physical-active-observed"
|
|
if observed_state == "scanning"
|
|
else "physical-standby-observed"
|
|
)
|
|
assert physical["record"]["resolution"] == expected_resolution
|
|
assert physical["requires_reconciliation"] is False
|
|
assert physical["resolved_active_recovery_required"] is (
|
|
observed_state == "scanning"
|
|
)
|
|
if observed_state == "scanning":
|
|
assert control["state"] == "scanning"
|
|
assert control["inspection_only"] is True
|
|
assert control["can_stop"] is True
|
|
assert control["can_start"] is False
|
|
assert acquisition["state"] == "failed"
|
|
assert acquisition["requested_streams"] == []
|
|
assert acquisition["result"]["recovery_only"] is True
|
|
assert settled["connection_attempt"]["safe_next_action"] == "stop-acquisition"
|
|
assert settled["connection_policy"]["actions"]["stop-acquisition"]["allowed"] is True
|
|
assert settled["connection_policy"]["actions"]["start-acquisition"]["allowed"] is False
|
|
authority = service._connection_scenario_reset_retired_transport_authority # noqa: SLF001
|
|
assert authority is not None
|
|
synthetic_standby = {
|
|
**physical,
|
|
"status": "resolved",
|
|
"requires_reconciliation": False,
|
|
"resolved_active_recovery_required": False,
|
|
"reconciled_physical_state": "standby",
|
|
"physical_active": False,
|
|
"observed_session_state": "ready",
|
|
"record": {
|
|
**physical["record"],
|
|
"resolution": "stop-standby-observed",
|
|
},
|
|
}
|
|
with service._lock: # noqa: SLF001
|
|
assert (
|
|
service._consumed_reset_authority_has_safe_reconfiguration_successor_locked( # noqa: SLF001
|
|
authority=authority,
|
|
physical=physical,
|
|
request=request,
|
|
)
|
|
is False
|
|
)
|
|
# Even after a later STOP proves standby, the old request itself
|
|
# still lacks a separately committed Change Network generation.
|
|
assert (
|
|
service._consumed_reset_authority_has_safe_reconfiguration_successor_locked( # noqa: SLF001
|
|
authority=authority,
|
|
physical=synthetic_standby,
|
|
request=request,
|
|
)
|
|
is False
|
|
)
|
|
service._connection_reconfiguration_revision += 1 # noqa: SLF001
|
|
service._connection_reconfiguration_intent_id = ( # noqa: SLF001
|
|
"change-network-after-explicit-stop"
|
|
)
|
|
service._connection_reconfiguration_intent = "change-network" # noqa: SLF001
|
|
service._connection_reconfiguration_required_transport_ref = "k1-a" # noqa: SLF001
|
|
service._connection_reconfiguration_required_connection_mode = "bridge" # noqa: SLF001
|
|
service._ble_discovery_generation += 1 # noqa: SLF001
|
|
service._connection_reconfiguration_minimum_discovery_generation = ( # noqa: SLF001
|
|
service._ble_discovery_generation # noqa: SLF001
|
|
)
|
|
successor_request = request.model_copy(
|
|
update={
|
|
"operation_id": None,
|
|
"ssid": "new-network-after-explicit-stop",
|
|
"idempotency_key": "new-key-after-explicit-stop",
|
|
"expected_discovery_generation": (
|
|
service._ble_discovery_generation # noqa: SLF001
|
|
),
|
|
"expected_reconfiguration_revision": (
|
|
service._connection_reconfiguration_revision # noqa: SLF001
|
|
),
|
|
"expected_reconfiguration_intent_id": (
|
|
service._connection_reconfiguration_intent_id # noqa: SLF001
|
|
),
|
|
}
|
|
)
|
|
assert service._consumed_reset_authority_has_safe_reconfiguration_successor_locked( # noqa: SLF001
|
|
authority=authority,
|
|
physical=synthetic_standby,
|
|
request=successor_request,
|
|
)
|
|
else:
|
|
assert control["state"] == "connection-ready"
|
|
assert control["inspection_only"] is True
|
|
assert control["inspection_promotion_allowed"] is False
|
|
assert settled["connection_attempt"]["safe_next_action"] == (
|
|
"start-acquisition"
|
|
)
|
|
assert successor is not None
|
|
assert successor["last_operation"]["status"] == "succeeded"
|
|
assert service._connection_scenario_reset_retired_transport_authority is None # noqa: SLF001
|
|
assert runtime.phase == "idle"
|
|
assert runtime.source_mode == "idle"
|
|
assert runtime.start_calls == []
|
|
assert service._acquisition_session_lease is None # noqa: SLF001
|
|
assert service.camera_preview.snapshot()["phase"] == "idle"
|
|
assert service._live_perception_camera_binding is None # noqa: SLF001
|
|
|
|
|
|
@pytest.mark.parametrize("supersession", ["intent", "runtime"])
|
|
def test_control_bootstrap_continuation_cannot_publish_stale_parent_proof(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
supersession: str,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
writes = _install_successful_fast_ack_bridge_write(monkeypatch)
|
|
request = _connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
idempotency_key=f"fast-ack-stale-{supersession}",
|
|
)
|
|
|
|
async def scenario() -> None:
|
|
endpoint_started = asyncio.Event()
|
|
release_endpoint = asyncio.Event()
|
|
|
|
async def blocked_endpoint(*_: object, **__: object) -> object:
|
|
endpoint_started.set()
|
|
await release_endpoint.wait()
|
|
return facade_module._CorrelatedEndpointObservation( # noqa: SLF001
|
|
path=_direct_host_path("192.168.68.50"),
|
|
reachable=True,
|
|
reason_code=None,
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
blocked_endpoint,
|
|
)
|
|
fast_ack = await service.connect(request)
|
|
assert endpoint_started.is_set()
|
|
with service._lock: # noqa: SLF001
|
|
continuation_task = service._control_bootstrap_continuation_task # noqa: SLF001
|
|
if supersession == "runtime":
|
|
service._snapshot_runtime_id = "snapshot-runtime-superseded" # noqa: SLF001
|
|
assert continuation_task is not None
|
|
if supersession == "intent":
|
|
service._connection_supervisor.set_intent( # noqa: SLF001
|
|
intent_id="explicit-new-intent",
|
|
requested_mode="bridge",
|
|
)
|
|
release_endpoint.set()
|
|
with pytest.raises(facade_module.ConnectionVerificationError):
|
|
await asyncio.wait_for(asyncio.shield(continuation_task), timeout=1.0)
|
|
state = service.state()
|
|
assert writes == ["network-write"]
|
|
assert fast_ack["connection_attempt"]["phase"] == "network_applied"
|
|
assert state["connection_attempt"]["phase"] == "network_applied"
|
|
assert state["connection_attempt"]["control_state"] == "control_not_ready"
|
|
assert state["connection_attempt"]["safe_next_action"] == ("verify-control-read-only")
|
|
assert state["network_mutation_ledger"]["resolution"] == "target-observed"
|
|
|
|
asyncio.run(scenario())
|
|
|
|
|
|
def test_service_close_cancels_owned_control_bootstrap_without_rewriting_network(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
writes = _install_successful_fast_ack_bridge_write(monkeypatch)
|
|
request = _connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
idempotency_key="fast-ack-service-close-owns-child",
|
|
)
|
|
|
|
async def scenario() -> None:
|
|
endpoint_started = asyncio.Event()
|
|
|
|
async def blocked_endpoint(*_: object, **__: object) -> object:
|
|
endpoint_started.set()
|
|
await asyncio.Event().wait()
|
|
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
blocked_endpoint,
|
|
)
|
|
fast_ack = await service.connect(request)
|
|
assert endpoint_started.is_set()
|
|
with service._lock: # noqa: SLF001
|
|
continuation_task = service._control_bootstrap_continuation_task # noqa: SLF001
|
|
continuation = service._control_bootstrap_continuation # noqa: SLF001
|
|
assert continuation_task is not None
|
|
assert continuation is not None
|
|
service.close()
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await continuation_task
|
|
child = service._operations.get(continuation.operation_id).as_dict() # noqa: SLF001
|
|
network = service._operations.get(continuation.parent_operation_id).as_dict() # noqa: SLF001
|
|
assert child["status"] == "cancelled"
|
|
assert network["status"] == "succeeded"
|
|
assert network["result"]["phase"] == "network_applied"
|
|
assert fast_ack["connection_attempt"]["phase"] == "network_applied"
|
|
assert writes == ["network-write"]
|
|
|
|
asyncio.run(scenario())
|
|
|
|
|
|
def test_control_bootstrap_final_fence_rejects_supersession_during_device_info(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
writes = _install_successful_fast_ack_bridge_write(monkeypatch)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
_reachable_control_endpoint,
|
|
)
|
|
control = FakeInteractiveControlSession(initial_state="idle")
|
|
original_open = control.open
|
|
|
|
def open_with_exact_identity(
|
|
*,
|
|
connection_binding: ApplicationConnectionBinding,
|
|
**kwargs: object,
|
|
) -> dict[str, object]:
|
|
snapshot = original_open(connection_binding=connection_binding, **kwargs)
|
|
intent = service._connection_supervisor.snapshot().intent # noqa: SLF001
|
|
assert intent is not None
|
|
control.verified_control = _verified_control_for_binding(
|
|
connection_binding,
|
|
logical_device_id=str(intent.expected_device_id or service._device_id), # noqa: SLF001
|
|
)
|
|
return snapshot
|
|
|
|
monkeypatch.setattr(control, "open", open_with_exact_identity)
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
production_bootstrap = MethodType(
|
|
XgridsK1CompatibilityService._bootstrap_prestart_control_ready_owned, # noqa: SLF001
|
|
service,
|
|
)
|
|
request = _connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
idempotency_key="fast-ack-device-info-final-fence",
|
|
)
|
|
|
|
async def scenario() -> None:
|
|
device_info_ready = asyncio.Event()
|
|
release_device_info = asyncio.Event()
|
|
|
|
async def ready_then_block(
|
|
_service: XgridsK1CompatibilityService,
|
|
*,
|
|
parent_operation_id: str | None,
|
|
connection_mode: facade_module.ConnectionMode,
|
|
inspection_only: bool = False,
|
|
) -> None:
|
|
await production_bootstrap(
|
|
parent_operation_id=parent_operation_id,
|
|
connection_mode=connection_mode,
|
|
inspection_only=inspection_only,
|
|
)
|
|
device_info_ready.set()
|
|
await release_device_info.wait()
|
|
|
|
service._bootstrap_prestart_control_ready_owned = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
ready_then_block,
|
|
service,
|
|
)
|
|
fast_ack = await service.connect(request)
|
|
await asyncio.wait_for(device_info_ready.wait(), timeout=1.0)
|
|
with service._lock: # noqa: SLF001
|
|
continuation_task = service._control_bootstrap_continuation_task # noqa: SLF001
|
|
continuation = service._control_bootstrap_continuation # noqa: SLF001
|
|
assert continuation_task is not None
|
|
assert continuation is not None
|
|
child_before_release = service._operations.get( # noqa: SLF001
|
|
continuation.operation_id
|
|
).as_dict()
|
|
assert child_before_release["status"] == "running"
|
|
|
|
service._connection_supervisor.record_monitor_failure( # noqa: SLF001
|
|
"test-route-lost-during-device-info"
|
|
)
|
|
release_device_info.set()
|
|
with pytest.raises(facade_module.ConnectionVerificationError):
|
|
await asyncio.wait_for(asyncio.shield(continuation_task), timeout=1.0)
|
|
|
|
child = service._operations.get(continuation.operation_id).as_dict() # noqa: SLF001
|
|
network = service._operations.get(continuation.parent_operation_id).as_dict() # noqa: SLF001
|
|
state = service.state()
|
|
assert child["status"] == "failed"
|
|
assert network["status"] == "succeeded"
|
|
assert network["result"]["phase"] == "network_applied"
|
|
assert fast_ack["connection_attempt"]["phase"] == "network_applied"
|
|
assert state["connection_attempt"]["control_state"] == "control_not_ready"
|
|
assert state["connection_attempt"]["safe_next_action"] != "start-acquisition"
|
|
assert control.state in {"closed", "idle"}
|
|
assert writes == ["network-write"]
|
|
|
|
asyncio.run(scenario())
|
|
|
|
|
|
def test_historical_bootstrap_success_loses_ready_projection_with_live_route(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
writes = _install_successful_fast_ack_bridge_write(monkeypatch)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
_reachable_control_endpoint,
|
|
)
|
|
request = _connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
idempotency_key="fast-ack-live-authority-loss",
|
|
)
|
|
|
|
async def scenario() -> None:
|
|
await service.connect(request)
|
|
with service._lock: # noqa: SLF001
|
|
continuation_task = service._control_bootstrap_continuation_task # noqa: SLF001
|
|
if continuation_task is not None:
|
|
await asyncio.wait_for(asyncio.shield(continuation_task), timeout=1.0)
|
|
ready = service.state()
|
|
assert ready["connection_attempt"]["control_state"] == "ready"
|
|
assert ready["connection_attempt"]["safe_next_action"] == "start-acquisition"
|
|
|
|
service._connection_supervisor.record_monitor_failure( # noqa: SLF001
|
|
"test-route-loss-after-bootstrap-success"
|
|
)
|
|
lost = service.state()
|
|
child = next(
|
|
item
|
|
for item in service._operations.snapshot() # noqa: SLF001
|
|
if item["action"] == facade_module.ACTION_CONNECTION_CONTROL_BOOTSTRAP
|
|
and item.get("context", {}).get("ownership") == "service-owned-apply-continuation"
|
|
)
|
|
assert child["status"] == "succeeded"
|
|
assert lost["connection_attempt"]["control_state"] == "control_not_ready"
|
|
assert lost["connection_attempt"]["safe_next_action"] != "start-acquisition"
|
|
assert lost["connection_lifecycle"]["ready_to_start"] is False
|
|
assert writes == ["network-write"]
|
|
|
|
asyncio.run(scenario())
|
|
|
|
|
|
def test_control_bootstrap_cancelled_before_first_step_is_never_left_accepted(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
writes = _install_successful_fast_ack_bridge_write(monkeypatch)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"_await_control_endpoint_reachable",
|
|
_reachable_control_endpoint,
|
|
)
|
|
request = _connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
idempotency_key="fast-ack-cancel-before-first-step",
|
|
)
|
|
|
|
async def scenario() -> None:
|
|
connected = await service.connect(request)
|
|
with service._lock: # noqa: SLF001
|
|
first_task = service._control_bootstrap_continuation_task # noqa: SLF001
|
|
if first_task is not None:
|
|
await asyncio.wait_for(asyncio.shield(first_task), timeout=1.0)
|
|
await asyncio.sleep(0)
|
|
network = next(
|
|
item
|
|
for item in connected["operations"]
|
|
if item["action"] == facade_module.ACTION_NETWORK_PROVISION
|
|
)
|
|
result = network["result"]
|
|
service._schedule_control_bootstrap_continuation( # noqa: SLF001
|
|
parent_operation_id=network["operation_id"],
|
|
connection_mode=result["connection_mode"],
|
|
transport_ref=result["transport_ref"],
|
|
target=facade_module.EndpointTarget(
|
|
result["target_ipv4"],
|
|
result["target_port"],
|
|
),
|
|
device_session_id=result["device_session_id"],
|
|
)
|
|
with service._lock: # noqa: SLF001
|
|
cancelled_task = service._control_bootstrap_continuation_task # noqa: SLF001
|
|
cancelled_continuation = service._control_bootstrap_continuation # noqa: SLF001
|
|
assert cancelled_task is not None
|
|
assert cancelled_continuation is not None
|
|
# No await has occurred since create_task: the coroutine has not entered.
|
|
service._request_control_bootstrap_continuation_close() # noqa: SLF001
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await cancelled_task
|
|
await asyncio.sleep(0)
|
|
child = service._operations.get( # noqa: SLF001
|
|
cancelled_continuation.operation_id
|
|
).as_dict()
|
|
assert child["status"] == "cancelled"
|
|
assert child["stage_code"] == "control-bootstrap-cancelled"
|
|
with service._lock: # noqa: SLF001
|
|
assert service._control_bootstrap_continuation is None # noqa: SLF001
|
|
assert service._control_bootstrap_continuation_task is None # noqa: SLF001
|
|
assert service._control_bootstrap_continuation_loop is None # noqa: SLF001
|
|
assert writes == ["network-write"]
|
|
|
|
asyncio.run(scenario())
|
|
|
|
|
|
def _seed_terminal_network_attempt(
|
|
service: XgridsK1CompatibilityService,
|
|
) -> None:
|
|
operation, created = service._operations.begin( # noqa: SLF001
|
|
facade_module.ACTION_NETWORK_PROVISION,
|
|
operation_id="op-00000000-0000-4000-8000-000000009901",
|
|
context={
|
|
"snapshot_runtime_id": service.state()["snapshot_runtime_id"],
|
|
"transport_ref": "terminal-attempt-k1",
|
|
"connection_mode": "bridge",
|
|
},
|
|
)
|
|
assert created is True
|
|
service._operations.transition( # noqa: SLF001
|
|
operation.operation_id,
|
|
"running",
|
|
stage_code="ble-provisioning-write",
|
|
message_code="connection.network.running",
|
|
)
|
|
service._operations.transition( # noqa: SLF001
|
|
operation.operation_id,
|
|
"succeeded",
|
|
stage_code="network-configured",
|
|
message_code="connection.network.succeeded",
|
|
result={
|
|
"phase": "network_applied",
|
|
"side_effect_status": "applied",
|
|
"automatic_retry": False,
|
|
},
|
|
)
|
|
|
|
|
|
def test_terminal_connection_attempt_projects_current_local_cleanup_not_wait(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
_seed_terminal_network_attempt(service)
|
|
runtime.source_mode = "live"
|
|
runtime.phase = "error"
|
|
|
|
state = service.state()
|
|
|
|
assert state["connection_attempt"]["status"] == "succeeded"
|
|
assert state["connection_policy"]["actions"]["stop-local-receiver"]["allowed"] is True
|
|
assert state["connection_attempt"]["safe_next_action"] == ("stop-local-receiver")
|
|
assert (
|
|
state["connection_attempt"]["diagnostic_bundle"]["attempt"]["safe_next_action"]
|
|
== "stop-local-receiver"
|
|
)
|
|
|
|
|
|
def test_terminal_connection_attempt_without_allowed_action_never_claims_wait_or_scan(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_terminal_network_attempt(service)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"ble_runtime_snapshot",
|
|
lambda: {
|
|
"owner_epoch": 9,
|
|
"owner_loop_bound": False,
|
|
"active_operation_kind": "status-read",
|
|
"cleanup_pending": True,
|
|
"poisoned": True,
|
|
},
|
|
)
|
|
|
|
state = service.state()
|
|
|
|
assert state["connection_attempt"]["status"] == "succeeded"
|
|
assert state["connection_policy"]["actions"]["scan-ble"]["allowed"] is False
|
|
assert state["connection_policy"]["actions"]["stop-local-receiver"]["allowed"] is False
|
|
assert state["connection_attempt"]["safe_next_action"] == ("manual-recovery-required")
|
|
|
|
|
|
def test_connection_attempt_projection_never_starts_without_ready_control() -> None:
|
|
runtime_id = "snapshot-runtime-test"
|
|
network_operation_id = "op-00000000-0000-4000-8000-000000000001"
|
|
network_operation = {
|
|
"operation_id": network_operation_id,
|
|
"action": facade_module.ACTION_NETWORK_PROVISION,
|
|
"status": "succeeded",
|
|
"stage_code": "network-configured",
|
|
"context": {
|
|
"snapshot_runtime_id": runtime_id,
|
|
"transport_ref": "k1-a",
|
|
"connection_mode": "bridge",
|
|
},
|
|
"result": {
|
|
"phase": "network_applied",
|
|
"control_state": "unknown",
|
|
"snapshot_runtime_id": runtime_id,
|
|
"parent_intent_id": network_operation_id,
|
|
"transport_ref": "k1-a",
|
|
"connection_mode": "bridge",
|
|
"target_ipv4": "192.168.68.50",
|
|
"target_port": facade_module.CONTROL_MQTT_PORT,
|
|
},
|
|
"events": [],
|
|
}
|
|
without_child = facade_module._connection_attempt_projection([network_operation]) # noqa: SLF001
|
|
assert without_child is not None
|
|
assert without_child["control_state"] == "unknown"
|
|
assert without_child["safe_next_action"] == "verify-control-read-only"
|
|
|
|
foreign_child = {
|
|
"operation_id": "op-00000000-0000-4000-8000-000000000002",
|
|
"action": facade_module.ACTION_CONNECTION_CONTROL_BOOTSTRAP,
|
|
"status": "running",
|
|
"stage_code": "mqtt-device-info",
|
|
"context": {
|
|
"parent_operation_id": network_operation["operation_id"],
|
|
"connection_mode": "bridge",
|
|
},
|
|
"events": [],
|
|
}
|
|
foreign = facade_module._connection_attempt_projection( # noqa: SLF001
|
|
[network_operation, foreign_child]
|
|
)
|
|
assert foreign is not None
|
|
assert foreign["safe_next_action"] == "verify-control-read-only"
|
|
|
|
owned_child = {
|
|
**foreign_child,
|
|
"context": {
|
|
"ownership": "service-owned-apply-continuation",
|
|
"snapshot_runtime_id": runtime_id,
|
|
"parent_operation_id": network_operation["operation_id"],
|
|
"parent_intent_id": network_operation["operation_id"],
|
|
"connection_mode": "bridge",
|
|
"transport_ref": "k1-a",
|
|
"target_ipv4": "192.168.68.50",
|
|
"target_port": facade_module.CONTROL_MQTT_PORT,
|
|
"device_session_id": "device-session-test",
|
|
"network_mutation_performed": False,
|
|
"ble_operation_performed": False,
|
|
"automatic_retry": False,
|
|
},
|
|
}
|
|
owned = facade_module._connection_attempt_projection( # noqa: SLF001
|
|
[network_operation, owned_child]
|
|
)
|
|
assert owned is not None
|
|
assert owned["control_state"] == "unknown"
|
|
assert owned["safe_next_action"] == "wait-for-current-attempt"
|
|
|
|
ready_child = {
|
|
**owned_child,
|
|
"status": "succeeded",
|
|
"result": {
|
|
"connection_mode": "bridge",
|
|
"control_verified": True,
|
|
"network_mutation_performed": False,
|
|
"ble_operation_performed": False,
|
|
"automatic_retry": False,
|
|
},
|
|
}
|
|
ready = facade_module._connection_attempt_projection( # noqa: SLF001
|
|
[network_operation, ready_child]
|
|
)
|
|
assert ready is not None
|
|
assert ready["control_state"] == "ready"
|
|
assert ready["safe_next_action"] == "start-acquisition"
|
|
|
|
scanning_child = {
|
|
**ready_child,
|
|
"result": {
|
|
**ready_child["result"],
|
|
"physical_reconciliation": {
|
|
"performed": True,
|
|
"resolution": "physical-active-observed",
|
|
"observed_session_state": "scanning",
|
|
"device_write_performed": False,
|
|
"automatic_retry": False,
|
|
},
|
|
},
|
|
}
|
|
scanning = facade_module._connection_attempt_projection( # noqa: SLF001
|
|
[network_operation, scanning_child]
|
|
)
|
|
assert scanning is not None
|
|
assert scanning["control_state"] == "unknown"
|
|
assert scanning["safe_next_action"] == "stop-acquisition"
|
|
assert scanning["safe_next_action"] != "start-acquisition"
|
|
assert scanning["physical_reconciliation"] == (
|
|
scanning_child["result"]["physical_reconciliation"]
|
|
)
|
|
|
|
foreign_verify = {
|
|
"operation_id": "op-00000000-0000-4000-8000-000000000003",
|
|
"action": facade_module.ACTION_CONNECTION_VERIFY,
|
|
"status": "succeeded",
|
|
"stage_code": "device-info-confirmed",
|
|
"context": {
|
|
"snapshot_runtime_id": runtime_id,
|
|
"requested_transport_ref": "other-k1",
|
|
"requested_connection_mode": "quick-connect",
|
|
},
|
|
"result": {
|
|
"write_performed": False,
|
|
"control_verified": True,
|
|
"verified_binding": {
|
|
"snapshot_runtime_id": runtime_id,
|
|
"intent_id": "foreign-intent",
|
|
"transport_ref": "other-k1",
|
|
"connection_mode": "quick-connect",
|
|
"target_ipv4": "192.168.43.1",
|
|
"target_port": facade_module.CONTROL_MQTT_PORT,
|
|
"host_path_epoch": 2,
|
|
},
|
|
},
|
|
"events": [],
|
|
}
|
|
foreign_verify_projection = facade_module._connection_attempt_projection( # noqa: SLF001
|
|
[network_operation, foreign_verify]
|
|
)
|
|
assert foreign_verify_projection is not None
|
|
assert foreign_verify_projection["control_state"] == "unknown"
|
|
assert foreign_verify_projection["safe_next_action"] == "verify-control-read-only"
|
|
|
|
exact_verify_context = {
|
|
"snapshot_runtime_id": runtime_id,
|
|
"recovery_parent_operation_id": network_operation_id,
|
|
"recovery_parent_intent_id": network_operation_id,
|
|
"recovery_transport_ref": "k1-a",
|
|
"recovery_connection_mode": "bridge",
|
|
"recovery_target_ipv4": "192.168.68.50",
|
|
"recovery_target_port": facade_module.CONTROL_MQTT_PORT,
|
|
}
|
|
exact_verify = {
|
|
**foreign_verify,
|
|
"status": "running",
|
|
"context": exact_verify_context,
|
|
"result": None,
|
|
}
|
|
exact_pending = facade_module._connection_attempt_projection( # noqa: SLF001
|
|
[network_operation, exact_verify]
|
|
)
|
|
assert exact_pending is not None
|
|
assert exact_pending["control_state"] == "unknown"
|
|
assert exact_pending["safe_next_action"] == "wait-for-current-attempt"
|
|
|
|
wrong_verify_binding = {
|
|
**exact_verify,
|
|
"status": "succeeded",
|
|
"result": foreign_verify["result"],
|
|
}
|
|
wrong_verify = facade_module._connection_attempt_projection( # noqa: SLF001
|
|
[network_operation, wrong_verify_binding]
|
|
)
|
|
assert wrong_verify is not None
|
|
assert wrong_verify["control_state"] == "unknown"
|
|
assert wrong_verify["safe_next_action"] == "verify-control-read-only"
|
|
|
|
exact_verify_success = {
|
|
**exact_verify,
|
|
"status": "succeeded",
|
|
"result": {
|
|
"write_performed": False,
|
|
"control_verified": True,
|
|
"verified_binding": {
|
|
"snapshot_runtime_id": runtime_id,
|
|
"intent_id": "durable-read-only-verify-intent",
|
|
"transport_ref": "k1-a",
|
|
"connection_mode": "bridge",
|
|
"target_ipv4": "192.168.68.50",
|
|
"target_port": facade_module.CONTROL_MQTT_PORT,
|
|
"host_path_epoch": 2,
|
|
},
|
|
},
|
|
}
|
|
exact_ready = facade_module._connection_attempt_projection( # noqa: SLF001
|
|
[network_operation, exact_verify_success]
|
|
)
|
|
assert exact_ready is not None
|
|
assert exact_ready["control_state"] == "ready"
|
|
assert exact_ready["safe_next_action"] == "start-acquisition"
|
|
|
|
|
|
def test_connect_reconfiguration_admission_fences_mode_selection_before_transition_gate(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Connect's pre-secret semantic admission is one deterministic winner."""
|
|
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
borrow_entered = threading.Event()
|
|
release_borrow = threading.Event()
|
|
connect_failures: list[BaseException] = []
|
|
device_writes: list[str] = []
|
|
|
|
@contextmanager
|
|
def paused_network_ble_borrow() -> Iterator[object]:
|
|
# Connect has passed its semantic admission and retains the
|
|
# reconfiguration gate across durable journal creation. Mode Select
|
|
# cannot publish a stale draft in this pre-transition interval.
|
|
borrow_entered.set()
|
|
assert release_borrow.wait(2.0)
|
|
raise RuntimeError("synthetic admitted Connect ended before BLE")
|
|
yield object() # pragma: no cover - contextmanager generator marker
|
|
|
|
async def forbidden_device_write(*_: object, **__: object) -> dict[str, Any]:
|
|
device_writes.append("gatt-write")
|
|
raise AssertionError("stale mode Connect crossed the GATT boundary")
|
|
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_borrow_network_ble_process_lease",
|
|
paused_network_ble_borrow,
|
|
)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_device_write)
|
|
request = _connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
idempotency_key="connect-admission-wins-before-mode-selection",
|
|
)
|
|
|
|
def run_connect() -> None:
|
|
try:
|
|
asyncio.run(service.connect(request))
|
|
except BaseException as exc: # pragma: no branch - expected contract edge
|
|
connect_failures.append(exc)
|
|
|
|
connect_thread = threading.Thread(target=run_connect, daemon=True)
|
|
connect_thread.start()
|
|
assert borrow_entered.wait(2.0)
|
|
try:
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as blocked:
|
|
service.select_connection_mode(
|
|
DesiredConnectionModeRequest(
|
|
connection_mode="quick-connect",
|
|
expected_revision=0,
|
|
)
|
|
)
|
|
assert blocked.value.reason_code == "connection-mode-selection-lifecycle-busy"
|
|
finally:
|
|
release_borrow.set()
|
|
connect_thread.join(timeout=3.0)
|
|
|
|
assert connect_thread.is_alive() is False
|
|
assert len(connect_failures) == 1
|
|
assert isinstance(connect_failures[0], RuntimeError)
|
|
assert str(connect_failures[0]) == "synthetic admitted Connect ended before BLE"
|
|
assert device_writes == []
|
|
state = service.state()
|
|
assert state["desired_connection_mode"] == "bridge"
|
|
assert state["desired_connection_mode_revision"] == 0
|
|
assert state["configured_connection_mode"] is None
|
|
assert state["active_connection_mode"] is None
|
|
operation = next(item for item in state["operations"] if item["action"] == "network.provision")
|
|
assert operation["status"] == "failed"
|
|
assert operation["error"]["side_effect_status"] == "none"
|
|
terminal_record = next(
|
|
record
|
|
for record in (
|
|
service._require_network_provisioning_idempotency_journal() # noqa: SLF001
|
|
.snapshot()
|
|
.records
|
|
)
|
|
if record.operation_id == operation["operation_id"]
|
|
)
|
|
terminal = terminal_record.terminal
|
|
assert terminal is not None
|
|
assert terminal.side_effect_status == "none"
|
|
assert terminal.safe_to_retry is True
|
|
|
|
|
|
def test_mode_selection_is_rejected_while_connect_owns_transition_gate(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Once Connect owns lifecycle admission its exact mode cannot change."""
|
|
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
capture_entered = threading.Event()
|
|
release_capture = threading.Event()
|
|
connect_failures: list[BaseException] = []
|
|
device_writes: list[str] = []
|
|
|
|
def paused_capture(_device_id: str) -> None:
|
|
# Capture is the first candidate-bound step after Connect acquired the
|
|
# lifecycle gate and repeated its exact mode/discovery CAS check.
|
|
capture_entered.set()
|
|
assert release_capture.wait(2.0)
|
|
return None
|
|
|
|
async def forbidden_device_write(*_: object, **__: object) -> dict[str, Any]:
|
|
device_writes.append("gatt-write")
|
|
raise AssertionError("test precondition must fail before GATT")
|
|
|
|
monkeypatch.setattr(facade_module, "_capture_network_intent_device", paused_capture)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_device_write)
|
|
request = _connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
idempotency_key="mode-cas-connect-owns-transition-gate",
|
|
)
|
|
|
|
def run_connect() -> None:
|
|
try:
|
|
asyncio.run(service.connect(request))
|
|
except BaseException as exc: # pragma: no branch - expected contract edge
|
|
connect_failures.append(exc)
|
|
|
|
connect_thread = threading.Thread(target=run_connect, daemon=True)
|
|
connect_thread.start()
|
|
assert capture_entered.wait(2.0)
|
|
try:
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as busy_select:
|
|
service.select_connection_mode(
|
|
DesiredConnectionModeRequest(
|
|
connection_mode="quick-connect",
|
|
expected_revision=0,
|
|
)
|
|
)
|
|
assert busy_select.value.reason_code == "connection-mode-selection-lifecycle-busy"
|
|
during_connect = service.state()
|
|
assert during_connect["desired_connection_mode"] == "bridge"
|
|
assert during_connect["desired_connection_mode_revision"] == 0
|
|
finally:
|
|
release_capture.set()
|
|
connect_thread.join(timeout=3.0)
|
|
|
|
assert connect_thread.is_alive() is False
|
|
assert len(connect_failures) == 1
|
|
assert isinstance(connect_failures[0], facade_module.NetworkProvisioningConflict)
|
|
assert connect_failures[0].reason_code == "network-provision-candidate-not-fresh"
|
|
assert device_writes == []
|
|
state = service.state()
|
|
assert state["desired_connection_mode"] == "bridge"
|
|
assert state["desired_connection_mode_revision"] == 0
|
|
operation = next(item for item in state["operations"] if item["action"] == "network.provision")
|
|
assert operation["status"] == "failed"
|
|
assert operation["error"]["side_effect_status"] == "none"
|
|
|
|
|
|
def test_mode_selection_winning_before_start_final_admission_publishes_no_start(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""A newer prepared-mode draft wins before START owns its final fence."""
|
|
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
_select_connection_mode(service, "quick-connect")
|
|
control = FakeInteractiveControlSession()
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
binding = _seed_supervised_connection(
|
|
service,
|
|
target_ipv4=facade_module.AP_FALLBACK_IPV4,
|
|
connection_mode="quick-connect",
|
|
transport_ref="k1-a",
|
|
)
|
|
control.verified_control = _verified_control_for_binding(binding)
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="TEST001",
|
|
host=facade_module.AP_FALLBACK_IPV4,
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
fingerprint_entered = threading.Event()
|
|
release_fingerprint = threading.Event()
|
|
start_failures: list[BaseException] = []
|
|
selection_results: list[dict[str, Any]] = []
|
|
selection_failures: list[BaseException] = []
|
|
original_fingerprint = service._request_fingerprint # noqa: SLF001
|
|
|
|
def paused_fingerprint(action: str, payload: dict[str, Any]) -> str:
|
|
if action == facade_module.ACTION_ACQUISITION_START:
|
|
fingerprint_entered.set()
|
|
assert release_fingerprint.wait(2.0)
|
|
return original_fingerprint(action, payload)
|
|
|
|
monkeypatch.setattr(service, "_request_fingerprint", paused_fingerprint)
|
|
|
|
def run_start() -> None:
|
|
try:
|
|
service.start_acquisition(
|
|
_start_request(
|
|
acquisition_id=acquisition_id,
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
)
|
|
except BaseException as exc: # pragma: no branch - expected race loser
|
|
start_failures.append(exc)
|
|
|
|
start_thread = threading.Thread(target=run_start, daemon=True)
|
|
start_thread.start()
|
|
assert fingerprint_entered.wait(2.0)
|
|
|
|
def run_selection() -> None:
|
|
try:
|
|
selection_results.append(
|
|
service.select_connection_mode(
|
|
DesiredConnectionModeRequest(
|
|
connection_mode="bridge",
|
|
expected_revision=1,
|
|
)
|
|
)
|
|
)
|
|
except BaseException as exc: # pragma: no cover - assertion aid
|
|
selection_failures.append(exc)
|
|
|
|
selection_thread = threading.Thread(target=run_selection, daemon=True)
|
|
selection_thread.start()
|
|
try:
|
|
deadline = time.monotonic() + 2.0
|
|
while time.monotonic() < deadline:
|
|
with service._lock: # noqa: SLF001
|
|
if service._desired_connection_mode_revision == 2: # noqa: SLF001
|
|
break
|
|
time.sleep(0.01)
|
|
with service._lock: # noqa: SLF001
|
|
assert service._desired_connection_mode == "bridge" # noqa: SLF001
|
|
assert service._desired_connection_mode_revision == 2 # noqa: SLF001
|
|
finally:
|
|
release_fingerprint.set()
|
|
start_thread.join(timeout=3.0)
|
|
selection_thread.join(timeout=3.0)
|
|
|
|
assert start_thread.is_alive() is False
|
|
assert selection_thread.is_alive() is False
|
|
assert selection_failures == []
|
|
assert selection_results[0]["desired_connection_mode"] == "bridge"
|
|
assert selection_results[0]["desired_connection_mode_revision"] == 2
|
|
assert len(start_failures) == 1
|
|
assert isinstance(start_failures[0], facade_module.NetworkProvisioningConflict)
|
|
assert start_failures[0].reason_code == "connection-mode-switch-pending"
|
|
assert control.start_projects == []
|
|
assert runtime.start_calls == []
|
|
state = service.state()
|
|
assert state["acquisition"]["state"] == "prepared"
|
|
start_operation = next(
|
|
item for item in state["operations"] if item["action"] == "acquisition.start"
|
|
)
|
|
assert start_operation["status"] == "failed"
|
|
assert start_operation["error"]["side_effect_status"] == "none"
|
|
|
|
|
|
def test_start_owned_and_queued_states_reject_mode_selection_until_dispatch_resolves(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Queued START remains a mode-selection fence after the HTTP call returns."""
|
|
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
_select_connection_mode(service, "quick-connect")
|
|
request_start_entered = threading.Event()
|
|
release_request_start = threading.Event()
|
|
|
|
class QueuedStartControl(FakeInteractiveControlSession):
|
|
def request_start(
|
|
self,
|
|
*,
|
|
project_name: str,
|
|
confirmation: object,
|
|
command_context: object,
|
|
preparation_checkpoint_observer: object | None = None,
|
|
expected_session_generation: int | None = None,
|
|
expected_state_revision: int | None = None,
|
|
) -> dict[str, object]:
|
|
del preparation_checkpoint_observer
|
|
assert confirmation is not None
|
|
assert command_context is not None
|
|
assert self.state == "project-ready"
|
|
self._accept_checkpoint(
|
|
expected_session_generation=expected_session_generation,
|
|
expected_state_revision=expected_state_revision,
|
|
)
|
|
self.start_projects.append(project_name)
|
|
self.start_contexts.append(command_context)
|
|
self.state = "start-requested"
|
|
request_start_entered.set()
|
|
assert release_request_start.wait(2.0)
|
|
# The real worker has only been released here; modeling:start has
|
|
# deliberately not been published in this deterministic fixture.
|
|
return self.snapshot()
|
|
|
|
control = QueuedStartControl()
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
binding = _seed_supervised_connection(
|
|
service,
|
|
target_ipv4=facade_module.AP_FALLBACK_IPV4,
|
|
connection_mode="quick-connect",
|
|
transport_ref="k1-a",
|
|
)
|
|
control.verified_control = _verified_control_for_binding(binding)
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="TEST001",
|
|
host=facade_module.AP_FALLBACK_IPV4,
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
start_failures: list[BaseException] = []
|
|
|
|
def run_start() -> None:
|
|
try:
|
|
service.start_acquisition(
|
|
_start_request(
|
|
acquisition_id=acquisition_id,
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
)
|
|
except BaseException as exc: # pragma: no cover - assertion aid
|
|
start_failures.append(exc)
|
|
|
|
start_thread = threading.Thread(target=run_start, daemon=True)
|
|
start_thread.start()
|
|
assert request_start_entered.wait(2.0)
|
|
try:
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as owned:
|
|
service.select_connection_mode(
|
|
DesiredConnectionModeRequest(
|
|
connection_mode="bridge",
|
|
expected_revision=1,
|
|
)
|
|
)
|
|
assert owned.value.reason_code == "connection-mode-selection-lifecycle-busy"
|
|
finally:
|
|
release_request_start.set()
|
|
start_thread.join(timeout=3.0)
|
|
|
|
assert start_thread.is_alive() is False
|
|
assert start_failures == []
|
|
queued = service.state()
|
|
assert queued["acquisition"]["state"] == "starting"
|
|
assert queued["desired_connection_mode"] == "quick-connect"
|
|
assert queued["desired_connection_mode_revision"] == 1
|
|
assert queued["connection_lifecycle"]["mode_selection"] == {
|
|
"allowed": False,
|
|
"reason_codes": [
|
|
"connection-mode-selection-physical-state-unsafe",
|
|
"connection-mode-selection-control-state-unsafe",
|
|
],
|
|
"automatic_retry": False,
|
|
}
|
|
assert "select-connection-mode" not in queued["connection_lifecycle"]["allowed_actions"]
|
|
assert runtime.start_calls
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as queued_select:
|
|
service.select_connection_mode(
|
|
DesiredConnectionModeRequest(
|
|
connection_mode="bridge",
|
|
expected_revision=1,
|
|
)
|
|
)
|
|
assert queued_select.value.reason_code == "connection-mode-selection-physical-state-unsafe"
|
|
assert service.state()["desired_connection_mode_revision"] == 1
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("source_mode", "target_mode", "target_ipv4", "source_attestation"),
|
|
[
|
|
(
|
|
"quick-connect",
|
|
"bridge",
|
|
facade_module.AP_FALLBACK_IPV4,
|
|
QUICK_CONNECT_ATTESTATION,
|
|
),
|
|
(
|
|
"bridge",
|
|
"quick-connect",
|
|
"192.168.1.20",
|
|
ATTESTATION,
|
|
),
|
|
],
|
|
ids=["quick-to-bridge", "bridge-to-quick"],
|
|
)
|
|
def test_pending_mode_scan_aborts_purely_local_prepared_acquisition_without_stop(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
source_mode: facade_module.ConnectionMode,
|
|
target_mode: facade_module.ConnectionMode,
|
|
target_ipv4: str,
|
|
source_attestation: CompatibilityAttestationRequest,
|
|
) -> None:
|
|
"""PREPARE without START is locally reversible at the explicit Scan commit."""
|
|
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
if source_mode != "bridge":
|
|
_select_connection_mode(service, source_mode)
|
|
source_revision = service.state()["desired_connection_mode_revision"]
|
|
control = FakeInteractiveControlSession()
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
binding = _seed_supervised_connection(
|
|
service,
|
|
target_ipv4=target_ipv4,
|
|
connection_mode=source_mode,
|
|
transport_ref="k1-a",
|
|
)
|
|
control.verified_control = _verified_control_for_binding(binding)
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="TEST001",
|
|
host=target_ipv4,
|
|
compatibility_attestation=source_attestation,
|
|
)
|
|
)
|
|
assert prepared["acquisition"]["state"] == "prepared"
|
|
assert control.state == "project-ready"
|
|
switched = service.select_connection_mode(
|
|
DesiredConnectionModeRequest(
|
|
connection_mode=target_mode,
|
|
expected_revision=source_revision,
|
|
)
|
|
)
|
|
assert switched["connection_policy"]["actions"]["scan-ble"]["allowed"] is True
|
|
assert switched["connection_lifecycle"]["mode_selection"]["allowed"] is True
|
|
assert "select-connection-mode" in switched["connection_lifecycle"]["allowed_actions"]
|
|
|
|
physical_or_network_edges: list[str] = []
|
|
|
|
async def fake_scan(
|
|
_duration_seconds: float,
|
|
*,
|
|
on_admitted: Callable[[], None] | None = None,
|
|
) -> dict[str, Any]:
|
|
assert service._acquisition is not None # noqa: SLF001
|
|
assert service._acquisition.state == "aborted" # noqa: SLF001
|
|
assert control.state == "idle"
|
|
assert on_admitted is not None
|
|
on_admitted()
|
|
return _ble_scan_result(f"{target_mode}-candidate")
|
|
|
|
async def forbidden_device_write(*_: object, **__: object) -> dict[str, Any]:
|
|
physical_or_network_edges.append("network-write")
|
|
raise AssertionError("prepared mode switch must not write K1 network state")
|
|
|
|
monkeypatch.setattr(facade_module, "scan", fake_scan)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_device_write)
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"prepare",
|
|
lambda *_args, **_kwargs: physical_or_network_edges.append("physical-command"),
|
|
)
|
|
|
|
scanned = asyncio.run(service.scan_ble(1.0))
|
|
|
|
assert physical_or_network_edges == []
|
|
assert control.start_projects == []
|
|
assert control.stop_calls == 0
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
assert scanned["acquisition"]["state"] == "aborted"
|
|
assert scanned["acquisition"]["result"] == {
|
|
"receiver_started": False,
|
|
"device_command_attempted": False,
|
|
"reason_code": "superseded-by-connection-mode-switch",
|
|
}
|
|
assert scanned["configured_connection_mode"] is None
|
|
assert scanned["active_connection_mode"] is None
|
|
assert [item["device_id"] for item in scanned["devices"]] == [f"{target_mode}-candidate"]
|
|
|
|
|
|
def test_mode_draft_can_be_cancelled_before_scan_without_touching_active_binding(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Quick -> Bridge draft -> Quick is two local CAS writes and no I/O."""
|
|
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
_select_connection_mode(service, "quick-connect")
|
|
control = FakeInteractiveControlSession(initial_state="project-ready")
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
binding = _seed_supervised_connection(
|
|
service,
|
|
target_ipv4=facade_module.AP_FALLBACK_IPV4,
|
|
connection_mode="quick-connect",
|
|
transport_ref="k1-a",
|
|
)
|
|
control.verified_control = _verified_control_for_binding(binding)
|
|
before = service.state()
|
|
before_supervisor_revision = service._connection_supervisor.snapshot().revision # noqa: SLF001
|
|
device_edges: list[str] = []
|
|
|
|
async def forbidden_scan(*_: object, **__: object) -> dict[str, Any]:
|
|
device_edges.append("ble-scan")
|
|
raise AssertionError("draft cancellation must not scan")
|
|
|
|
async def forbidden_network_write(*_: object, **__: object) -> dict[str, Any]:
|
|
device_edges.append("network-write")
|
|
raise AssertionError("draft cancellation must not write network state")
|
|
|
|
monkeypatch.setattr(facade_module, "scan", forbidden_scan)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_network_write)
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"prepare",
|
|
lambda *_args, **_kwargs: device_edges.append("physical-command"),
|
|
)
|
|
|
|
switched = service.select_connection_mode(
|
|
DesiredConnectionModeRequest(
|
|
connection_mode="bridge",
|
|
expected_revision=before["desired_connection_mode_revision"],
|
|
)
|
|
)
|
|
cancelled = service.select_connection_mode(
|
|
DesiredConnectionModeRequest(
|
|
connection_mode="quick-connect",
|
|
expected_revision=switched["desired_connection_mode_revision"],
|
|
)
|
|
)
|
|
|
|
assert device_edges == []
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
assert control.state == "project-ready"
|
|
assert control.start_projects == []
|
|
assert control.stop_calls == 0
|
|
assert cancelled["desired_connection_mode_revision"] == (
|
|
before["desired_connection_mode_revision"] + 2
|
|
)
|
|
assert cancelled["desired_connection_mode"] == "quick-connect"
|
|
assert cancelled["configured_connection_mode"] == "quick-connect"
|
|
assert cancelled["active_connection_mode"] == "quick-connect"
|
|
assert (
|
|
cancelled["connection_lifecycle"]["active_binding_key"]
|
|
== before["connection_lifecycle"]["active_binding_key"]
|
|
)
|
|
assert (
|
|
cancelled["connection_lifecycle"]["active_binding"]
|
|
== before["connection_lifecycle"]["active_binding"]
|
|
)
|
|
assert cancelled["connection_lifecycle"]["mode_change"]["state"] == "ready"
|
|
assert cancelled["connection_lifecycle"]["connection_ready"] is True
|
|
assert cancelled["connection_lifecycle"]["ready_to_start"] is True
|
|
assert cancelled["connection_lifecycle"]["mode_selection"]["allowed"] is True
|
|
assert service._connection_supervisor.snapshot().revision == before_supervisor_revision # noqa: SLF001
|
|
|
|
|
|
def test_lifecycle_mode_selection_is_disallowed_while_acquiring(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
_select_connection_mode(service, "quick-connect")
|
|
control = FakeInteractiveControlSession()
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
binding = _seed_supervised_connection(
|
|
service,
|
|
target_ipv4=facade_module.AP_FALLBACK_IPV4,
|
|
connection_mode="quick-connect",
|
|
transport_ref="k1-a",
|
|
)
|
|
control.verified_control = _verified_control_for_binding(binding)
|
|
service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="TEST001",
|
|
host=facade_module.AP_FALLBACK_IPV4,
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
)
|
|
)
|
|
with service._lock: # noqa: SLF001
|
|
assert service._acquisition is not None # noqa: SLF001
|
|
service._acquisition.transition( # noqa: SLF001
|
|
"starting",
|
|
message_code="acquisition.start.waiting_receiver_ready",
|
|
)
|
|
service._acquisition.transition( # noqa: SLF001
|
|
"acquiring",
|
|
message_code="acquisition.acquiring",
|
|
)
|
|
service._acquisition_start_operation_id = "test-start-operation" # noqa: SLF001
|
|
runtime.source_mode = "live"
|
|
runtime.phase = "live"
|
|
control.state = "scanning"
|
|
|
|
state = service.state()
|
|
|
|
assert state["connection_lifecycle"]["mode_selection"]["allowed"] is False
|
|
assert (
|
|
"connection-mode-selection-physical-state-unsafe"
|
|
in state["connection_lifecycle"]["mode_selection"]["reason_codes"]
|
|
)
|
|
assert (
|
|
"connection-mode-selection-control-state-unsafe"
|
|
in state["connection_lifecycle"]["mode_selection"]["reason_codes"]
|
|
)
|
|
assert "select-connection-mode" not in state["connection_lifecycle"]["allowed_actions"]
|
|
|
|
|
|
def test_lifecycle_mode_selection_is_disallowed_by_unsafe_physical_ledger(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
control = FakeInteractiveControlSession(initial_state="connection-ready")
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
binding = _seed_supervised_connection(service, connection_mode="bridge")
|
|
control.verified_control = _verified_control_for_binding(binding)
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"snapshot",
|
|
lambda: {
|
|
"status": "unresolved",
|
|
"requires_reconciliation": True,
|
|
"record": None,
|
|
},
|
|
)
|
|
|
|
state = service.state()
|
|
|
|
assert state["connection_lifecycle"]["mode_selection"] == {
|
|
"allowed": False,
|
|
"reason_codes": ["connection-mode-selection-physical-state-unsafe"],
|
|
"automatic_retry": False,
|
|
}
|
|
assert "select-connection-mode" not in state["connection_lifecycle"]["allowed_actions"]
|
|
|
|
|
|
def test_disconnected_mode_draft_ignores_old_physical_history_without_io(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""A cold selector is local; the old ledger still fences later actions."""
|
|
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
control = FakeInteractiveControlSession(initial_state="idle")
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
physical_snapshot = {
|
|
"status": "unresolved",
|
|
"requires_reconciliation": True,
|
|
"resolved_active_recovery_required": False,
|
|
"record": {
|
|
"operation_id": "old-stop",
|
|
"action": "stop",
|
|
"stage": "observing",
|
|
"resolution": None,
|
|
},
|
|
}
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"snapshot",
|
|
lambda: physical_snapshot,
|
|
)
|
|
device_edges: list[str] = []
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"scan",
|
|
lambda *_args, **_kwargs: device_edges.append("scan"),
|
|
)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"provision_wifi_once",
|
|
lambda *_args, **_kwargs: device_edges.append("network-write"),
|
|
)
|
|
|
|
before = service.state()
|
|
assert before["connection_lifecycle"]["mode_selection"]["allowed"] is True
|
|
|
|
selected = service.select_connection_mode(
|
|
DesiredConnectionModeRequest(
|
|
connection_mode="quick-connect",
|
|
expected_revision=before["desired_connection_mode_revision"],
|
|
)
|
|
)
|
|
|
|
assert selected["desired_connection_mode"] == "quick-connect"
|
|
assert selected["desired_connection_mode_revision"] == 1
|
|
assert selected["physical_command"]["requires_reconciliation"] is True
|
|
assert selected["connection_policy"]["actions"]["provision-fresh-device"]["allowed"] is False
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
assert control.state == "idle"
|
|
assert device_edges == []
|
|
|
|
|
|
def test_carried_old_reopen_audit_does_not_freeze_a_successor_mode_draft() -> None:
|
|
physical_snapshot: dict[str, object] = {
|
|
"status": "unresolved",
|
|
"requires_reconciliation": True,
|
|
"record": {
|
|
"operation_id": "successor-stop",
|
|
"revision": 41,
|
|
"stage": "observing",
|
|
"resolution": None,
|
|
"operator_retirements": [
|
|
{
|
|
"retirement_id": "old-retirement",
|
|
"original_attempt": {"operation_id": "old-stop"},
|
|
}
|
|
],
|
|
"operator_reconciliation_reopens": [
|
|
{
|
|
"retirement_id": "old-retirement",
|
|
"retired_record_revision": 12,
|
|
}
|
|
],
|
|
},
|
|
}
|
|
|
|
assert (
|
|
facade_module._physical_command_pending_reopened_reconciliation_mode( # noqa: SLF001
|
|
physical_snapshot
|
|
)
|
|
is None
|
|
)
|
|
assert (
|
|
facade_module._connection_mode_selection_reason_codes( # noqa: SLF001
|
|
acquisition_state=None,
|
|
acquisition_lease_retained=False,
|
|
acquisition_start_operation_id=None,
|
|
runtime={"source_mode": "idle"},
|
|
physical_command=physical_snapshot,
|
|
control_state="idle",
|
|
desired_connection_mode="bridge",
|
|
)
|
|
== []
|
|
)
|
|
|
|
|
|
_RECOVERY_VENDOR_HASH = "a" * 64
|
|
_RECOVERY_SERIAL_HASH = "b" * 64
|
|
_RECOVERY_PROJECT_HASH = "c" * 64
|
|
_RECOVERY_ACQUISITION_ID = "acquisition-persisted-active-k1"
|
|
_RECOVERY_START_OPERATION_ID = "physical-start-persisted-active-k1"
|
|
|
|
|
|
def _persist_resolved_active_start_for_restart(
|
|
service: XgridsK1CompatibilityService,
|
|
) -> None:
|
|
"""Persist one successful START without retaining process-local acquisition."""
|
|
|
|
identity = PhysicalCommandIdentity(
|
|
vendor_device_id_sha256=_RECOVERY_VENDOR_HASH,
|
|
device_serial_sha256=_RECOVERY_SERIAL_HASH,
|
|
)
|
|
connection = PhysicalCommandConnectionBinding(
|
|
intent_id="old-start-intent",
|
|
transport_ref="test-ble-transport",
|
|
connection_mode="bridge",
|
|
target_ipv4="192.168.68.52",
|
|
target_port=facade_module.CONTROL_MQTT_PORT,
|
|
host_path_epoch=1,
|
|
control_session_id="old-start-control",
|
|
producer_generation=1,
|
|
)
|
|
|
|
def status(
|
|
session_state: str,
|
|
*,
|
|
observed_at_utc: str,
|
|
) -> PhysicalCommandStatusEvidence:
|
|
project_bound = session_state == "scanning"
|
|
return PhysicalCommandStatusEvidence(
|
|
source="live-control-session",
|
|
vendor_device_id_sha256=_RECOVERY_VENDOR_HASH,
|
|
device_serial_sha256=_RECOVERY_SERIAL_HASH,
|
|
control_session_id=connection.control_session_id,
|
|
host_path_epoch=connection.host_path_epoch,
|
|
producer_generation=connection.producer_generation,
|
|
session_state=session_state, # type: ignore[arg-type]
|
|
session_state_code=302 if session_state == "scanning" else 300,
|
|
project_bound=project_bound,
|
|
project_id_sha256=_RECOVERY_PROJECT_HASH if project_bound else None,
|
|
init_ready=project_bound,
|
|
status_message_sha256=hashlib.sha256(
|
|
f"{session_state}:{observed_at_utc}".encode()
|
|
).hexdigest(),
|
|
mqtt_retained=False,
|
|
observed_at_utc=observed_at_utc,
|
|
)
|
|
|
|
ledger = service._physical_command_ledger # noqa: SLF001
|
|
ledger.prepare(
|
|
operation_id=_RECOVERY_START_OPERATION_ID,
|
|
parent_operation_id=None,
|
|
acquisition_id=_RECOVERY_ACQUISITION_ID,
|
|
action="start",
|
|
identity=identity,
|
|
connection=connection,
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
payload_sha256="d" * 64,
|
|
baseline_status=status(
|
|
"ready",
|
|
observed_at_utc="2026-08-09T12:00:00.000Z",
|
|
),
|
|
)
|
|
ledger.mark_dispatching(_RECOVERY_START_OPERATION_ID)
|
|
ledger.mark_observing(
|
|
_RECOVERY_START_OPERATION_ID,
|
|
publish_call_returned=True,
|
|
packet_id=41,
|
|
)
|
|
ledger.mark_qos2_completed(_RECOVERY_START_OPERATION_ID, packet_id=41)
|
|
ledger.record_application_response(
|
|
_RECOVERY_START_OPERATION_ID,
|
|
PhysicalCommandApplicationResponse(
|
|
operation_id=_RECOVERY_START_OPERATION_ID,
|
|
action="start",
|
|
control_session_id=connection.control_session_id,
|
|
host_path_epoch=connection.host_path_epoch,
|
|
producer_generation=connection.producer_generation,
|
|
result_code=PHYSICAL_COMMAND_APPLICATION_SUCCESS_CODE,
|
|
success=True,
|
|
payload_sha256="e" * 64,
|
|
observed_at_utc="2026-08-09T12:00:01.000Z",
|
|
),
|
|
)
|
|
ledger.record_status_observation(
|
|
_RECOVERY_START_OPERATION_ID,
|
|
status("scanning", observed_at_utc="2026-08-09T12:00:02.000Z"),
|
|
)
|
|
ledger.resolve(
|
|
_RECOVERY_START_OPERATION_ID,
|
|
resolution="start-active-observed",
|
|
)
|
|
|
|
|
|
def _persist_resolved_unclassified_stop_for_restart(
|
|
service: XgridsK1CompatibilityService,
|
|
) -> tuple[str, int]:
|
|
"""Persist the exact legacy startup shape without process-local owners."""
|
|
|
|
_persist_resolved_active_start_for_restart(service)
|
|
ledger = service._physical_command_ledger # noqa: SLF001
|
|
start = ledger.snapshot().record
|
|
assert start is not None
|
|
assert start.last_status is not None
|
|
stop_operation_id = "physical-stop-resolved-unclassified-k1"
|
|
ledger.prepare(
|
|
operation_id=stop_operation_id,
|
|
parent_operation_id=start.operation_id,
|
|
acquisition_id=start.acquisition_id,
|
|
action="stop",
|
|
identity=start.identity,
|
|
connection=start.connection,
|
|
compatibility_profile_id=start.compatibility_profile_id,
|
|
payload_sha256="6" * 64,
|
|
baseline_status=start.last_status,
|
|
)
|
|
resolved = ledger.resolve(
|
|
stop_operation_id,
|
|
resolution="not-dispatched",
|
|
)
|
|
assert resolved.resolved_unclassified_stop_recovery_required is True
|
|
return stop_operation_id, resolved.revision
|
|
|
|
|
|
def _persist_classified_ready_stop_for_restart(
|
|
service: XgridsK1CompatibilityService,
|
|
) -> tuple[str, PhysicalCommandConnectionBinding]:
|
|
"""Persist S0 plus one read-only READY proof on an obsolete C1 binding."""
|
|
|
|
stop_operation_id, _ = _persist_resolved_unclassified_stop_for_restart(service)
|
|
coordinator = service._physical_command_coordinator # noqa: SLF001
|
|
observed_at_utc = datetime.now(UTC).isoformat(timespec="milliseconds").replace(
|
|
"+00:00",
|
|
"Z",
|
|
)
|
|
c1 = PhysicalCommandRuntimeBinding(
|
|
vendor_device_id_sha256=_RECOVERY_VENDOR_HASH,
|
|
device_serial_sha256=_RECOVERY_SERIAL_HASH,
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
intent_id="classified-ready-c1-intent",
|
|
transport_ref="test-ble-transport",
|
|
connection_mode="bridge",
|
|
target_ipv4="192.168.68.52",
|
|
target_port=facade_module.CONTROL_MQTT_PORT,
|
|
host_path_epoch=7,
|
|
control_session_id="classified-ready-c1-control",
|
|
producer_generation=7,
|
|
)
|
|
coordinator.application_response(
|
|
ApplicationMqttResponseEvidence(
|
|
operation_key="bootstrap:classified-ready-c1:DeviceInfoRequest",
|
|
response_topic="lixel/application/response/device_info",
|
|
payload_sha256="4" * 64,
|
|
modeling_action=None,
|
|
result_code=None,
|
|
success=None,
|
|
observed_at_utc=observed_at_utc,
|
|
)
|
|
)
|
|
coordinator.bind_control_session(c1)
|
|
coordinator.device_status(
|
|
ApplicationMqttDeviceStatusEvidence(
|
|
vendor_device_id_sha256=_RECOVERY_VENDOR_HASH,
|
|
device_serial_sha256=_RECOVERY_SERIAL_HASH,
|
|
session_state="ready",
|
|
session_state_code=MODELING_STATE_BASE + 300,
|
|
project_bound=False,
|
|
project_id_sha256=None,
|
|
init_ready=False,
|
|
status_message_sha256="5" * 64,
|
|
mqtt_retained=False,
|
|
observed_at_utc=observed_at_utc,
|
|
)
|
|
)
|
|
classified = coordinator.reconcile_unresolved(
|
|
reconciliation_id="classified-ready-c1",
|
|
)
|
|
assert classified["operation_id"] == stop_operation_id
|
|
assert classified["resolution"] == "not-dispatched"
|
|
return stop_operation_id, PhysicalCommandConnectionBinding(
|
|
intent_id=c1.intent_id,
|
|
transport_ref=c1.transport_ref,
|
|
connection_mode=c1.connection_mode,
|
|
target_ipv4=c1.target_ipv4,
|
|
target_port=c1.target_port,
|
|
host_path_epoch=c1.host_path_epoch,
|
|
control_session_id=c1.control_session_id,
|
|
producer_generation=c1.producer_generation,
|
|
)
|
|
|
|
|
|
def test_resolved_active_power_loss_exposes_local_retirement_and_revokes_target(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
_persist_resolved_active_start_for_restart(service)
|
|
_seed_terminal_network_attempt(service)
|
|
before = service.state()
|
|
checkpoint = before["physical_command"]["operator_retirement"]
|
|
action = before["connection_policy"]["actions"]["retire-unavailable-physical-target"]
|
|
io_calls: list[str] = []
|
|
|
|
async def forbidden_io(*_args: object, **_kwargs: object) -> object:
|
|
io_calls.append("device-or-network-io")
|
|
raise AssertionError("resolved-active retirement must stay local-only")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_io)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_io)
|
|
monkeypatch.setattr(
|
|
service,
|
|
"_probe_control_endpoint",
|
|
lambda *_args, **_kwargs: io_calls.append("tcp-probe"),
|
|
)
|
|
|
|
assert before["physical_command"]["resolved_active_recovery_required"] is True
|
|
assert checkpoint["allowed"] is True
|
|
assert action["allowed"] is True
|
|
assert action["target_source"] == "durable-physical-command"
|
|
assert action["required_transport_ref"] == "test-ble-transport"
|
|
assert before["connection_policy"]["recommended_action"] == (
|
|
"retire-unavailable-physical-target"
|
|
)
|
|
assert before["connection_attempt"]["safe_next_action"] == (
|
|
"retire-unavailable-physical-target"
|
|
)
|
|
|
|
retired = service.retire_unavailable_physical_command(
|
|
RetireUnavailablePhysicalCommandRequest(
|
|
retirement_id="retirement-resolved-active-power-loss",
|
|
expected_operation_id=checkpoint["expected_operation_id"],
|
|
expected_revision=checkpoint["expected_revision"],
|
|
expected_transport_ref=checkpoint["expected_transport_ref"],
|
|
operator_confirmed=True,
|
|
reason="device-permanently-unavailable-or-replaced",
|
|
)
|
|
)
|
|
|
|
assert retired["physical_command"]["record"]["resolution"] == (
|
|
"operator-retired-outcome-unknown"
|
|
)
|
|
assert retired["physical_command"]["operator_retirement"]["allowed"] is False
|
|
assert retired["selected_device_id"] is None
|
|
assert retired["k1_ip"] is None
|
|
assert retired["connection_policy"]["facts"]["retired_transport_refs"] == ["test-ble-transport"]
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
assert io_calls == []
|
|
|
|
|
|
class _VerifyPhysicalRecoveryCoordinator:
|
|
"""Small exact-proof coordinator double for facade Verify recovery tests."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
observed_session_state: str | None,
|
|
reconciliation_ready: bool,
|
|
prepared_stop_operation_id: str | None = None,
|
|
) -> None:
|
|
self.observed_session_state = observed_session_state
|
|
self.reconciliation_ready = reconciliation_ready
|
|
self.resolution: str | None = None
|
|
self.reconciliation_ids: list[str] = []
|
|
self.verified_control: dict[str, object] | None = None
|
|
self.prepared_stop_operation_id = prepared_stop_operation_id
|
|
|
|
def _resolved_record(self, reconciliation_id: str) -> dict[str, object]:
|
|
assert self.verified_control is not None
|
|
connection = {
|
|
field: self.verified_control[field]
|
|
for field in (
|
|
"intent_id",
|
|
"transport_ref",
|
|
"connection_mode",
|
|
"target_ipv4",
|
|
"target_port",
|
|
"host_path_epoch",
|
|
"control_session_id",
|
|
"producer_generation",
|
|
)
|
|
}
|
|
return {
|
|
"operation_id": self.prepared_stop_operation_id or "persisted-start-observing",
|
|
"acquisition_id": "acq-persisted-start",
|
|
"action": "stop" if self.prepared_stop_operation_id is not None else "start",
|
|
"stage": "resolved",
|
|
"resolution": (
|
|
"not-dispatched"
|
|
if self.prepared_stop_operation_id is not None
|
|
else self.resolution
|
|
),
|
|
"compatibility_profile_id": XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
"connection": {
|
|
"transport_ref": "test-ble-transport",
|
|
"connection_mode": "bridge",
|
|
},
|
|
"reconciliations": [
|
|
{
|
|
"reconciliation_id": reconciliation_id,
|
|
"kind": (
|
|
"prepared-stop-classification"
|
|
if self.prepared_stop_operation_id is not None
|
|
else "ambiguous-outcome"
|
|
),
|
|
"resolution": "physical-active-observed",
|
|
"original_attempt": {
|
|
"operation_id": (
|
|
self.prepared_stop_operation_id or "persisted-start-observing"
|
|
),
|
|
"action": (
|
|
"stop" if self.prepared_stop_operation_id is not None else "start"
|
|
),
|
|
},
|
|
"verified_binding": {"connection": connection},
|
|
"observation": {
|
|
"source": "explicit-read-only-reconciliation",
|
|
"control_session_id": self.verified_control["control_session_id"],
|
|
"host_path_epoch": self.verified_control["host_path_epoch"],
|
|
"producer_generation": self.verified_control["producer_generation"],
|
|
"session_state": "scanning",
|
|
"project_bound": True,
|
|
"project_id_sha256": "8" * 64,
|
|
"init_ready": True,
|
|
"mqtt_retained": False,
|
|
},
|
|
}
|
|
],
|
|
}
|
|
|
|
def snapshot(self) -> dict[str, object]:
|
|
resolved_active_recovery_required = self.resolution == "physical-active-observed"
|
|
reconciliations = [{"resolution": self.resolution}] if self.resolution is not None else []
|
|
return {
|
|
"status": "resolved" if self.resolution is not None else "unresolved",
|
|
"reason_code": (
|
|
None if self.resolution is not None else "physical-command-reconciliation-required"
|
|
),
|
|
"requires_reconciliation": self.resolution is None,
|
|
"resolved_active_recovery_required": resolved_active_recovery_required,
|
|
"automatic_replay_allowed": False,
|
|
"normal_session_recovery_supported": False,
|
|
"recovery_requirement": (
|
|
"explicit-read-only-deviceinfo-and-non-retained-devicestatus"
|
|
if self.resolution is None or resolved_active_recovery_required
|
|
else None
|
|
),
|
|
"runtime_bound": True,
|
|
"reconciliation_ready": (
|
|
(self.resolution is None or resolved_active_recovery_required)
|
|
and self.reconciliation_ready
|
|
),
|
|
"observed_session_state": self.observed_session_state,
|
|
"active_operation_id": None,
|
|
"record": {
|
|
"operation_id": self.prepared_stop_operation_id or "persisted-start-observing",
|
|
"action": "stop" if self.prepared_stop_operation_id is not None else "start",
|
|
"stage": "resolved" if self.resolution is not None else "observing",
|
|
"resolution": self.resolution,
|
|
"connection": {
|
|
"transport_ref": "test-ble-transport",
|
|
"connection_mode": "bridge",
|
|
},
|
|
"reconciliations": reconciliations,
|
|
},
|
|
}
|
|
|
|
def reconcile_unresolved(self, *, reconciliation_id: str) -> dict[str, object]:
|
|
assert self.reconciliation_ready is True
|
|
assert self.observed_session_state in {"ready", "scanning"}
|
|
self.reconciliation_ids.append(reconciliation_id)
|
|
self.resolution = (
|
|
"physical-standby-observed"
|
|
if self.observed_session_state == "ready"
|
|
else "physical-active-observed"
|
|
)
|
|
return self._resolved_record(reconciliation_id)
|
|
|
|
def reconcile_resolved_active(
|
|
self,
|
|
*,
|
|
reconciliation_id: str,
|
|
) -> dict[str, object]:
|
|
assert self.resolution == "physical-active-observed"
|
|
assert self.reconciliation_ready is True
|
|
assert self.observed_session_state in {"ready", "scanning"}
|
|
self.reconciliation_ids.append(reconciliation_id)
|
|
if self.observed_session_state == "ready":
|
|
self.resolution = "physical-standby-observed"
|
|
return self._resolved_record(reconciliation_id)
|
|
|
|
|
|
def _install_synthetic_verify_recovery(
|
|
service: XgridsK1CompatibilityService,
|
|
coordinator: _VerifyPhysicalRecoveryCoordinator,
|
|
) -> None:
|
|
"""Install a no-I/O topology+DeviceInfo path while keeping real gates."""
|
|
|
|
_seed_supervised_connection(service, connection_mode="bridge", with_control=False)
|
|
with service._lock: # noqa: SLF001
|
|
service._connection_verification = { # noqa: SLF001
|
|
"lease_generation": service._connection_lease_generation, # noqa: SLF001
|
|
"lease_state": "reachable",
|
|
}
|
|
service._physical_command_coordinator = coordinator # type: ignore[assignment] # noqa: SLF001
|
|
|
|
async def verify_topology_without_io(
|
|
bound_service: XgridsK1CompatibilityService,
|
|
request: ConnectionVerifyRequest,
|
|
) -> tuple[str, bool, None]:
|
|
operation, created = bound_service._operations.begin( # noqa: SLF001
|
|
facade_module.ACTION_CONNECTION_VERIFY,
|
|
operation_id=request.operation_id,
|
|
deadline_seconds=10.0,
|
|
)
|
|
if created:
|
|
bound_service._operations.transition( # noqa: SLF001
|
|
operation.operation_id,
|
|
"running",
|
|
stage_code="topology-confirmed-device-info-pending",
|
|
message_code="connection.verify.device_info_pending",
|
|
)
|
|
return operation.operation_id, created, None
|
|
|
|
async def bootstrap_with_process_holder(
|
|
bound_service: XgridsK1CompatibilityService,
|
|
*,
|
|
parent_operation_id: str | None,
|
|
connection_mode: facade_module.ConnectionMode,
|
|
inspection_only: bool = False,
|
|
) -> None:
|
|
await _synthetic_prestart_control_bootstrap(
|
|
bound_service,
|
|
parent_operation_id=parent_operation_id,
|
|
connection_mode=connection_mode,
|
|
inspection_only=inspection_only,
|
|
)
|
|
verified = bound_service._application_control_session.snapshot().get( # noqa: SLF001
|
|
"verified_control"
|
|
)
|
|
assert isinstance(verified, dict)
|
|
coordinator.verified_control = dict(verified)
|
|
bound_service._acquire_application_control_process_lease() # noqa: SLF001
|
|
|
|
service._verify_connection_owned = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
verify_topology_without_io,
|
|
service,
|
|
)
|
|
service._bootstrap_prestart_control_ready_owned = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
bootstrap_with_process_holder,
|
|
service,
|
|
)
|
|
|
|
|
|
def _install_real_coordinator_bootstrap(
|
|
service: XgridsK1CompatibilityService,
|
|
*,
|
|
observed_session_state: str,
|
|
) -> None:
|
|
"""Install exact DeviceInfo/status evidence on the facade's real topology path."""
|
|
|
|
assert observed_session_state in {"ready", "scanning"}
|
|
coordinator = service._physical_command_coordinator # noqa: SLF001
|
|
|
|
async def bootstrap_with_real_coordinator(
|
|
bound_service: XgridsK1CompatibilityService,
|
|
*,
|
|
parent_operation_id: str | None,
|
|
connection_mode: facade_module.ConnectionMode,
|
|
inspection_only: bool = False,
|
|
) -> None:
|
|
coordinator.prepare_read_only_bootstrap()
|
|
coordinator.application_response(
|
|
ApplicationMqttResponseEvidence(
|
|
operation_key=(
|
|
f"bootstrap:{parent_operation_id}:DeviceInfoRequest"
|
|
),
|
|
response_topic="lixel/application/response/device_info",
|
|
payload_sha256="7" * 64,
|
|
modeling_action=None,
|
|
result_code=None,
|
|
success=None,
|
|
observed_at_utc="2026-08-13T08:00:00.000Z",
|
|
)
|
|
)
|
|
await _synthetic_prestart_control_bootstrap(
|
|
bound_service,
|
|
parent_operation_id=parent_operation_id,
|
|
connection_mode=connection_mode,
|
|
inspection_only=inspection_only,
|
|
)
|
|
verified = bound_service._application_control_session.snapshot().get( # noqa: SLF001
|
|
"verified_control"
|
|
)
|
|
assert isinstance(verified, dict)
|
|
coordinator.bind_control_session(
|
|
PhysicalCommandRuntimeBinding(
|
|
vendor_device_id_sha256=_RECOVERY_VENDOR_HASH,
|
|
device_serial_sha256=_RECOVERY_SERIAL_HASH,
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
intent_id=str(verified["intent_id"]),
|
|
transport_ref=str(verified["transport_ref"]),
|
|
connection_mode=verified["connection_mode"], # type: ignore[arg-type]
|
|
target_ipv4=str(verified["target_ipv4"]),
|
|
target_port=int(verified["target_port"]),
|
|
host_path_epoch=int(verified["host_path_epoch"]),
|
|
control_session_id=str(verified["control_session_id"]),
|
|
producer_generation=int(verified["producer_generation"]),
|
|
)
|
|
)
|
|
scanning = observed_session_state == "scanning"
|
|
coordinator.device_status(
|
|
ApplicationMqttDeviceStatusEvidence(
|
|
vendor_device_id_sha256=_RECOVERY_VENDOR_HASH,
|
|
device_serial_sha256=_RECOVERY_SERIAL_HASH,
|
|
session_state=observed_session_state, # type: ignore[arg-type]
|
|
session_state_code=MODELING_STATE_BASE + (302 if scanning else 300),
|
|
project_bound=scanning,
|
|
project_id_sha256=_RECOVERY_PROJECT_HASH if scanning else None,
|
|
init_ready=scanning,
|
|
status_message_sha256="8" * 64,
|
|
mqtt_retained=False,
|
|
observed_at_utc="2026-08-13T08:00:01.000Z",
|
|
)
|
|
)
|
|
bound_service._acquire_application_control_process_lease() # noqa: SLF001
|
|
|
|
service._bootstrap_prestart_control_ready_owned = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
bootstrap_with_real_coordinator,
|
|
service,
|
|
)
|
|
|
|
|
|
def _install_real_coordinator_verify_recovery(
|
|
service: XgridsK1CompatibilityService,
|
|
*,
|
|
observed_session_state: str,
|
|
) -> None:
|
|
"""Drive public Verify with real durable reconciliation and no topology I/O."""
|
|
|
|
assert observed_session_state in {"ready", "scanning"}
|
|
_seed_supervised_connection(service, connection_mode="bridge", with_control=False)
|
|
with service._lock: # noqa: SLF001
|
|
service._connection_verification = { # noqa: SLF001
|
|
"lease_generation": service._connection_lease_generation, # noqa: SLF001
|
|
"lease_state": "reachable",
|
|
}
|
|
|
|
async def verify_topology_without_io(
|
|
bound_service: XgridsK1CompatibilityService,
|
|
request: ConnectionVerifyRequest,
|
|
) -> tuple[str, bool, None]:
|
|
operation, created = bound_service._operations.begin( # noqa: SLF001
|
|
facade_module.ACTION_CONNECTION_VERIFY,
|
|
operation_id=request.operation_id,
|
|
deadline_seconds=10.0,
|
|
)
|
|
if created:
|
|
bound_service._operations.transition( # noqa: SLF001
|
|
operation.operation_id,
|
|
"running",
|
|
stage_code="topology-confirmed-device-info-pending",
|
|
message_code="connection.verify.device_info_pending",
|
|
)
|
|
return operation.operation_id, created, None
|
|
|
|
service._verify_connection_owned = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
verify_topology_without_io,
|
|
service,
|
|
)
|
|
_install_real_coordinator_bootstrap(
|
|
service,
|
|
observed_session_state=observed_session_state,
|
|
)
|
|
|
|
|
|
def _retained_physical_recovery_verify_request(
|
|
*,
|
|
operation_id: str,
|
|
) -> ConnectionVerifyRequest:
|
|
"""Address the exact process-retained K1 seeded by recovery fixtures."""
|
|
|
|
return ConnectionVerifyRequest(
|
|
device_id="test-ble-transport",
|
|
source="retained-current-process",
|
|
compatibility_attestation=ATTESTATION,
|
|
operation_id=operation_id,
|
|
)
|
|
|
|
|
|
def _install_terminal_camera_error_residual(
|
|
service: XgridsK1CompatibilityService,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
*,
|
|
cleanup_failure: Exception | None = None,
|
|
producer_alive: bool = False,
|
|
) -> list[str]:
|
|
"""Reproduce the inert camera/process residue left by archive failure."""
|
|
|
|
source_id = facade_module.DEFAULT_ACQUISITION_CAMERA_SOURCE
|
|
camera_state = dict(service.camera_preview.snapshot())
|
|
camera_state.update(
|
|
{
|
|
"phase": "error",
|
|
"generation": 17,
|
|
"active_source_id": source_id,
|
|
"delivery": None,
|
|
"error": {
|
|
"code": "segment-too-large",
|
|
"message": "synthetic terminal camera archive failure",
|
|
},
|
|
}
|
|
)
|
|
camera_state["recording"] = {
|
|
"active": False,
|
|
"source_end_expected": False,
|
|
"session": None,
|
|
"active_epoch": None,
|
|
"committed_media_segment_count": None,
|
|
"producer_alive": producer_alive,
|
|
"producer_age_ms": None,
|
|
"last_segment_age_ms": None,
|
|
"preview_consumer_count": 0,
|
|
"completed_epochs": 1,
|
|
"last_summary": None,
|
|
}
|
|
camera_gate = threading.RLock()
|
|
cleanup_calls: list[str] = []
|
|
|
|
def snapshot() -> dict[str, Any]:
|
|
with camera_gate:
|
|
copied = dict(camera_state)
|
|
copied["recording"] = dict(camera_state["recording"])
|
|
error = camera_state.get("error")
|
|
copied["error"] = dict(error) if isinstance(error, dict) else None
|
|
return copied
|
|
|
|
def stop_current() -> dict[str, Any]:
|
|
cleanup_calls.append("stop-current")
|
|
if cleanup_failure is not None:
|
|
raise cleanup_failure
|
|
with camera_gate:
|
|
camera_state.update(
|
|
{
|
|
"phase": "idle",
|
|
"generation": None,
|
|
"active_source_id": None,
|
|
"delivery": None,
|
|
"error": None,
|
|
}
|
|
)
|
|
return snapshot()
|
|
|
|
monkeypatch.setattr(service.camera_preview, "snapshot", snapshot)
|
|
monkeypatch.setattr(service.camera_preview, "stop_current", stop_current)
|
|
assert service._ensure_camera_preview_process_lease() is True # noqa: SLF001
|
|
with service._lock: # noqa: SLF001
|
|
service._live_perception_camera_binding = ( # noqa: SLF001
|
|
"camera-failed-session",
|
|
source_id,
|
|
17,
|
|
)
|
|
service._camera_activation_lineage = ( # noqa: SLF001
|
|
"acq-camera-failed",
|
|
"camera-failed-session",
|
|
0,
|
|
)
|
|
service._camera_activation_retry_lineage = ( # noqa: SLF001
|
|
"acq-camera-failed",
|
|
"camera-failed-session",
|
|
0,
|
|
)
|
|
service._camera_activation_retry_not_before_monotonic = 42.0 # noqa: SLF001
|
|
return cleanup_calls
|
|
|
|
|
|
def test_explicit_verify_reconciles_persisted_start_to_ready_without_device_io(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
coordinator = _VerifyPhysicalRecoveryCoordinator(
|
|
observed_session_state="ready",
|
|
reconciliation_ready=True,
|
|
)
|
|
_install_synthetic_verify_recovery(service, coordinator)
|
|
operation_id = "op-00000000-0000-4000-8000-000000001201"
|
|
|
|
verify_state = asyncio.run(
|
|
service.verify_connection(
|
|
_retained_physical_recovery_verify_request(operation_id=operation_id)
|
|
)
|
|
)
|
|
state = service.state()
|
|
|
|
assert coordinator.reconciliation_ids == [f"{operation_id}.physical"]
|
|
assert state["physical_command"]["requires_reconciliation"] is False
|
|
assert state["physical_command"]["record"]["resolution"] == ("physical-standby-observed")
|
|
assert state["connection_lifecycle"]["mode_selection"]["allowed"] is True, state[
|
|
"connection_lifecycle"
|
|
]["mode_selection"]
|
|
assert verify_state["last_operation"]["status"] == "succeeded"
|
|
assert verify_state["last_operation"]["stage_code"] == ("physical-reconciliation-confirmed")
|
|
assert verify_state["last_operation"]["result"]["physical_reconciliation"] == {
|
|
"performed": True,
|
|
"resolution": "physical-standby-observed",
|
|
"observed_session_state": "ready",
|
|
"device_write_performed": False,
|
|
"automatic_retry": False,
|
|
}
|
|
assert service._application_control_process_lease_holders == {"control"} # noqa: SLF001
|
|
|
|
|
|
def test_long_first_verify_refreshes_aged_transport_before_exposing_prestart_control(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""A long checkpoint reconciliation cannot orphan its new ready socket."""
|
|
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
coordinator = _VerifyPhysicalRecoveryCoordinator(
|
|
observed_session_state="ready",
|
|
reconciliation_ready=True,
|
|
)
|
|
_install_synthetic_verify_recovery(service, coordinator)
|
|
supervisor = service._connection_supervisor # noqa: SLF001
|
|
monotonic_now = [100.0]
|
|
suspend_aware_now = [1_000.0]
|
|
supervisor._monotonic_clock = lambda: monotonic_now[0] # noqa: SLF001
|
|
supervisor._suspend_aware_clock = lambda: suspend_aware_now[0] # noqa: SLF001
|
|
before = supervisor.snapshot()
|
|
assert before.intent is not None
|
|
assert before.device_network.target is not None
|
|
host_epoch = supervisor.observe_host_path(
|
|
_association_bound_direct_host_path(before.device_network.target.ipv4)
|
|
)
|
|
assert supervisor.observe_endpoint(
|
|
target=before.device_network.target,
|
|
intent_id=before.intent.intent_id,
|
|
host_path_epoch=host_epoch,
|
|
reachable=True,
|
|
)
|
|
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
tcp_probes: list[str] = []
|
|
|
|
def reachable(target: str) -> bool:
|
|
tcp_probes.append(target)
|
|
return True
|
|
|
|
monkeypatch.setattr(facade_module, "_control_endpoint_reachable", reachable)
|
|
reconcile = service._reconcile_physical_command_after_verify_owned # noqa: SLF001
|
|
|
|
async def reconcile_after_transport_ages(
|
|
bound_service: XgridsK1CompatibilityService,
|
|
*,
|
|
verify_operation_id: str,
|
|
allow_receiver_rehydrate: bool = True,
|
|
) -> dict[str, Any]:
|
|
result = await reconcile(
|
|
verify_operation_id=verify_operation_id,
|
|
allow_receiver_rehydrate=allow_receiver_rehydrate,
|
|
)
|
|
monotonic_now[0] += 20.0
|
|
suspend_aware_now[0] += 20.0
|
|
return result
|
|
|
|
service._reconcile_physical_command_after_verify_owned = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
reconcile_after_transport_ages,
|
|
service,
|
|
)
|
|
operation_id = "op-00000000-0000-4000-8000-000000001202"
|
|
|
|
verified = asyncio.run(
|
|
service.verify_connection(
|
|
_retained_physical_recovery_verify_request(operation_id=operation_id)
|
|
)
|
|
)
|
|
|
|
assert tcp_probes == ["192.168.1.20"]
|
|
assert verified["last_operation"]["status"] == "succeeded"
|
|
assert verified["active_connection_mode"] == "bridge"
|
|
assert verified["connection_policy"]["actions"]["start-acquisition"]["allowed"] is True
|
|
assert service._application_control_session.snapshot()["state"] == "connection-ready" # noqa: SLF001
|
|
assert service._application_control_process_lease_holders == {"control"} # noqa: SLF001
|
|
fresh = supervisor.snapshot()
|
|
assert fresh.intent is not None
|
|
assert fresh.device_network.target is not None
|
|
assert supervisor.endpoint_observation_has_remaining_lease(
|
|
target=fresh.device_network.target,
|
|
intent_id=fresh.intent.intent_id,
|
|
host_path_epoch=fresh.host_path.epoch,
|
|
minimum_remaining_seconds=(
|
|
facade_module.VERIFY_POST_TRANSITION_ENDPOINT_MIN_REMAINING_SECONDS
|
|
),
|
|
)
|
|
|
|
|
|
def test_explicit_verify_reconciliation_does_not_require_normal_command_authority(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Fresh DeviceInfo/READY resolves the old edge before command authority exists."""
|
|
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
coordinator = _VerifyPhysicalRecoveryCoordinator(
|
|
observed_session_state="ready",
|
|
reconciliation_ready=True,
|
|
)
|
|
_install_synthetic_verify_recovery(service, coordinator)
|
|
bootstrap = service._bootstrap_prestart_control_ready_owned # noqa: SLF001
|
|
reconciliation_validations: list[str] = []
|
|
|
|
async def bootstrap_with_command_authority_blocked(
|
|
bound_service: XgridsK1CompatibilityService,
|
|
*,
|
|
parent_operation_id: str | None,
|
|
connection_mode: facade_module.ConnectionMode,
|
|
inspection_only: bool = False,
|
|
) -> None:
|
|
await bootstrap(
|
|
parent_operation_id=parent_operation_id,
|
|
connection_mode=connection_mode,
|
|
inspection_only=inspection_only,
|
|
)
|
|
control = bound_service._application_control_session # noqa: SLF001
|
|
|
|
def reject_normal_command_authority() -> None:
|
|
raise RuntimeError("normal command authority remains blocked")
|
|
|
|
def accept_exact_read_only_reconciliation() -> None:
|
|
reconciliation_validations.append("read-only")
|
|
|
|
control.validate_connection_binding = reject_normal_command_authority # type: ignore[method-assign]
|
|
control.validate_physical_reconciliation_binding = ( # type: ignore[method-assign]
|
|
accept_exact_read_only_reconciliation
|
|
)
|
|
|
|
service._bootstrap_prestart_control_ready_owned = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
bootstrap_with_command_authority_blocked,
|
|
service,
|
|
)
|
|
operation_id = "op-00000000-0000-4000-8000-000000001212"
|
|
|
|
result = asyncio.run(
|
|
service.verify_connection(
|
|
_retained_physical_recovery_verify_request(operation_id=operation_id)
|
|
)
|
|
)
|
|
|
|
assert reconciliation_validations == ["read-only", "read-only"]
|
|
assert result["last_operation"]["status"] == "succeeded"
|
|
assert result["physical_command"]["record"]["resolution"] == ("physical-standby-observed")
|
|
|
|
|
|
def test_explicit_verify_reconciliation_rechecks_binding_before_durable_commit(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""A route/control change between proof and CAS leaves the ledger unresolved."""
|
|
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
coordinator = _VerifyPhysicalRecoveryCoordinator(
|
|
observed_session_state="ready",
|
|
reconciliation_ready=True,
|
|
)
|
|
_install_synthetic_verify_recovery(service, coordinator)
|
|
bootstrap = service._bootstrap_prestart_control_ready_owned # noqa: SLF001
|
|
validation_count = 0
|
|
|
|
async def bootstrap_with_binding_change(
|
|
bound_service: XgridsK1CompatibilityService,
|
|
*,
|
|
parent_operation_id: str | None,
|
|
connection_mode: facade_module.ConnectionMode,
|
|
inspection_only: bool = False,
|
|
) -> None:
|
|
await bootstrap(
|
|
parent_operation_id=parent_operation_id,
|
|
connection_mode=connection_mode,
|
|
inspection_only=inspection_only,
|
|
)
|
|
control = bound_service._application_control_session # noqa: SLF001
|
|
|
|
def validate_until_commit() -> None:
|
|
nonlocal validation_count
|
|
validation_count += 1
|
|
if validation_count == 2:
|
|
raise RuntimeError("control generation changed before commit")
|
|
|
|
control.validate_physical_reconciliation_binding = ( # type: ignore[method-assign]
|
|
validate_until_commit
|
|
)
|
|
|
|
service._bootstrap_prestart_control_ready_owned = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
bootstrap_with_binding_change,
|
|
service,
|
|
)
|
|
operation_id = "op-00000000-0000-4000-8000-000000001213"
|
|
|
|
with pytest.raises(facade_module.ConnectionVerificationError) as failure:
|
|
asyncio.run(
|
|
service.verify_connection(
|
|
_retained_physical_recovery_verify_request(operation_id=operation_id)
|
|
)
|
|
)
|
|
|
|
assert failure.value.reason_code == "physical-command-reconciliation-failed"
|
|
assert validation_count == 2
|
|
assert coordinator.reconciliation_ids == []
|
|
physical = coordinator.snapshot()
|
|
assert physical["requires_reconciliation"] is True
|
|
assert physical["record"]["resolution"] is None
|
|
operation = service.state()["last_operation"]
|
|
assert operation["operation_id"] == operation_id
|
|
assert operation["status"] == "failed"
|
|
|
|
|
|
def test_explicit_verify_reconciles_scanning_as_active_without_unblocking_mode_change(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
coordinator = _VerifyPhysicalRecoveryCoordinator(
|
|
observed_session_state="scanning",
|
|
reconciliation_ready=True,
|
|
)
|
|
_install_synthetic_verify_recovery(service, coordinator)
|
|
|
|
state = asyncio.run(
|
|
service.verify_connection(
|
|
_retained_physical_recovery_verify_request(
|
|
operation_id="op-00000000-0000-4000-8000-000000001202"
|
|
)
|
|
)
|
|
)
|
|
|
|
assert state["physical_command"]["requires_reconciliation"] is False
|
|
assert state["physical_command"]["record"]["resolution"] == ("physical-active-observed")
|
|
assert state["connection_lifecycle"]["mode_selection"]["allowed"] is False
|
|
assert (
|
|
"connection-mode-selection-physical-state-unsafe"
|
|
in state["connection_lifecycle"]["mode_selection"]["reason_codes"]
|
|
)
|
|
assert (
|
|
"physical-device-already-active"
|
|
not in state["connection_policy"]["actions"]["scan-ble"]["reason_codes"]
|
|
)
|
|
assert (
|
|
"physical-device-already-active"
|
|
in state["connection_policy"]["actions"]["provision-fresh-device"]["reason_codes"]
|
|
)
|
|
assert state["acquisition"]["acquisition_id"] == "acq-persisted-start"
|
|
assert state["acquisition"]["state"] == "failed"
|
|
assert state["acquisition"]["cleanup_pending"] is False
|
|
assert state["acquisition"]["requested_streams"] == []
|
|
assert state["acquisition"]["result"]["recovery_only"] is True
|
|
assert state["acquisition"]["result"]["automatic_replay_allowed"] is False
|
|
assert state["application_control_session"]["can_stop"] is True
|
|
assert state["last_operation"]["result"]["physical_reconciliation"] == {
|
|
"performed": True,
|
|
"resolution": "physical-active-observed",
|
|
"observed_session_state": "scanning",
|
|
"device_write_performed": False,
|
|
"automatic_retry": False,
|
|
}
|
|
|
|
|
|
def test_explicit_verify_scanning_cleans_terminal_camera_residual_for_stop_only_shell(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
coordinator = _VerifyPhysicalRecoveryCoordinator(
|
|
observed_session_state="scanning",
|
|
reconciliation_ready=True,
|
|
)
|
|
_install_synthetic_verify_recovery(service, coordinator)
|
|
cleanup_calls = _install_terminal_camera_error_residual(service, monkeypatch)
|
|
operation_id = "op-00000000-0000-4000-8000-000000001214"
|
|
|
|
state = asyncio.run(
|
|
service.verify_connection(
|
|
_retained_physical_recovery_verify_request(operation_id=operation_id)
|
|
)
|
|
)
|
|
|
|
control = service._application_control_session # noqa: SLF001
|
|
assert isinstance(control, FakeInteractiveControlSession)
|
|
assert cleanup_calls == ["stop-current"]
|
|
assert coordinator.reconciliation_ids == [f"{operation_id}.physical"]
|
|
assert state["physical_command"]["record"]["resolution"] == (
|
|
"physical-active-observed"
|
|
)
|
|
assert state["acquisition"]["acquisition_id"] == "acq-persisted-start"
|
|
assert state["acquisition"]["state"] == "failed"
|
|
assert state["acquisition"]["result"]["recovery_only"] is True
|
|
assert state["application_control_session"]["can_stop"] is True
|
|
assert service.camera_preview.snapshot()["phase"] == "idle"
|
|
assert service._live_perception_camera_binding is None # noqa: SLF001
|
|
assert service._camera_activation_lineage is None # noqa: SLF001
|
|
assert service._camera_activation_retry_lineage is None # noqa: SLF001
|
|
assert service._camera_activation_retry_not_before_monotonic == 0.0 # noqa: SLF001
|
|
assert service._application_control_process_lease_holders == { # noqa: SLF001
|
|
"control"
|
|
}
|
|
assert control.start_projects == []
|
|
assert control.stop_calls == 0
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
|
|
|
|
def test_explicit_verify_scanning_camera_cleanup_failure_retains_fence(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
coordinator = _VerifyPhysicalRecoveryCoordinator(
|
|
observed_session_state="scanning",
|
|
reconciliation_ready=True,
|
|
)
|
|
_install_synthetic_verify_recovery(service, coordinator)
|
|
cleanup_calls = _install_terminal_camera_error_residual(
|
|
service,
|
|
monkeypatch,
|
|
cleanup_failure=RuntimeError("synthetic terminal camera cleanup failed"),
|
|
)
|
|
operation_id = "op-00000000-0000-4000-8000-000000001215"
|
|
|
|
with pytest.raises(facade_module.ConnectionVerificationError) as failure:
|
|
asyncio.run(
|
|
service.verify_connection(
|
|
_retained_physical_recovery_verify_request(operation_id=operation_id)
|
|
)
|
|
)
|
|
|
|
state = service.state()
|
|
control = service._application_control_session # noqa: SLF001
|
|
assert isinstance(control, FakeInteractiveControlSession)
|
|
assert failure.value.reason_code == "physical-command-reconciliation-failed"
|
|
assert cleanup_calls == ["stop-current"]
|
|
assert coordinator.reconciliation_ids == [f"{operation_id}.physical"]
|
|
assert state["physical_command"]["record"]["resolution"] == (
|
|
"physical-active-observed"
|
|
)
|
|
assert state["acquisition"] is None
|
|
assert state["last_operation"]["operation_id"] == operation_id
|
|
assert state["last_operation"]["status"] == "failed"
|
|
assert service.camera_preview.snapshot()["phase"] == "error"
|
|
assert service._live_perception_camera_binding is not None # noqa: SLF001
|
|
assert service._camera_activation_lineage is not None # noqa: SLF001
|
|
assert service._camera_activation_retry_lineage is not None # noqa: SLF001
|
|
assert service._application_control_process_lease_holders == { # noqa: SLF001
|
|
"camera"
|
|
}
|
|
assert control.start_projects == []
|
|
assert control.stop_calls == 0
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
|
|
|
|
def test_explicit_verify_scanning_refuses_to_cleanup_live_camera_producer(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
coordinator = _VerifyPhysicalRecoveryCoordinator(
|
|
observed_session_state="scanning",
|
|
reconciliation_ready=True,
|
|
)
|
|
_install_synthetic_verify_recovery(service, coordinator)
|
|
cleanup_calls = _install_terminal_camera_error_residual(
|
|
service,
|
|
monkeypatch,
|
|
producer_alive=True,
|
|
)
|
|
operation_id = "op-00000000-0000-4000-8000-000000001216"
|
|
|
|
with pytest.raises(facade_module.ConnectionVerificationError) as failure:
|
|
asyncio.run(
|
|
service.verify_connection(
|
|
_retained_physical_recovery_verify_request(operation_id=operation_id)
|
|
)
|
|
)
|
|
|
|
control = service._application_control_session # noqa: SLF001
|
|
assert isinstance(control, FakeInteractiveControlSession)
|
|
assert failure.value.reason_code == "physical-command-reconciliation-failed"
|
|
assert cleanup_calls == []
|
|
assert service._acquisition is None # noqa: SLF001
|
|
assert service.camera_preview.snapshot()["recording"]["producer_alive"] is True
|
|
assert service._application_control_process_lease_holders == { # noqa: SLF001
|
|
"camera"
|
|
}
|
|
assert control.start_projects == []
|
|
assert control.stop_calls == 0
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
|
|
|
|
def test_fresh_service_real_verify_rotation_rehydrates_one_stop_only_target(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
original, _ = service_with_fake_runtime(tmp_path)
|
|
_persist_resolved_active_start_for_restart(original)
|
|
assert original._acquisition is None # noqa: SLF001
|
|
|
|
# A genuinely fresh facade starts with no process-local acquisition. Its
|
|
# only physical authority is the persisted resolved-active START record.
|
|
restarted, _ = service_with_fake_runtime(tmp_path)
|
|
assert restarted._acquisition is None # noqa: SLF001
|
|
assert restarted.state()["physical_command"]["resolved_active_recovery_required"] is True
|
|
_set_scanned_k1(restarted, device_id="test-ble-transport")
|
|
|
|
async def read_status(
|
|
device_id: str,
|
|
*,
|
|
timeout_seconds: float,
|
|
rediscover: bool,
|
|
**_: object,
|
|
) -> dict[str, Any]:
|
|
assert (device_id, timeout_seconds, rediscover) == (
|
|
"test-ble-transport",
|
|
20.0,
|
|
False,
|
|
)
|
|
return _wifi_status_read("10.255.254.77", device_id=device_id)
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", read_status)
|
|
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)
|
|
monkeypatch.setattr(facade_module, "_inspect_host_path", _direct_host_path)
|
|
|
|
coordinator = restarted._physical_command_coordinator # noqa: SLF001
|
|
published_physical_edges: list[str] = []
|
|
|
|
async def bootstrap_recovery_control(
|
|
bound_service: XgridsK1CompatibilityService,
|
|
*,
|
|
parent_operation_id: str | None,
|
|
connection_mode: facade_module.ConnectionMode,
|
|
inspection_only: bool = False,
|
|
) -> None:
|
|
await _synthetic_prestart_control_bootstrap(
|
|
bound_service,
|
|
parent_operation_id=parent_operation_id,
|
|
connection_mode=connection_mode,
|
|
inspection_only=inspection_only,
|
|
)
|
|
control = bound_service._application_control_session # noqa: SLF001
|
|
verified = control.snapshot().get("verified_control")
|
|
assert isinstance(verified, dict)
|
|
coordinator.application_response(
|
|
ApplicationMqttResponseEvidence(
|
|
operation_key="bootstrap:1:DeviceInfoRequest",
|
|
response_topic="lixel/application/response/device_info",
|
|
payload_sha256="f" * 64,
|
|
modeling_action=None,
|
|
result_code=None,
|
|
success=None,
|
|
observed_at_utc="2026-08-09T12:10:00.000Z",
|
|
)
|
|
)
|
|
coordinator.bind_control_session(
|
|
PhysicalCommandRuntimeBinding(
|
|
vendor_device_id_sha256=_RECOVERY_VENDOR_HASH,
|
|
device_serial_sha256=_RECOVERY_SERIAL_HASH,
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
intent_id=str(verified["intent_id"]),
|
|
transport_ref=str(verified["transport_ref"]),
|
|
connection_mode=verified["connection_mode"], # type: ignore[arg-type]
|
|
target_ipv4=str(verified["target_ipv4"]),
|
|
target_port=int(verified["target_port"]),
|
|
host_path_epoch=int(verified["host_path_epoch"]),
|
|
control_session_id=str(verified["control_session_id"]),
|
|
producer_generation=int(verified["producer_generation"]),
|
|
)
|
|
)
|
|
coordinator.device_status(
|
|
ApplicationMqttDeviceStatusEvidence(
|
|
vendor_device_id_sha256=_RECOVERY_VENDOR_HASH,
|
|
device_serial_sha256=_RECOVERY_SERIAL_HASH,
|
|
session_state="scanning",
|
|
session_state_code=MODELING_STATE_BASE + 302,
|
|
project_bound=True,
|
|
project_id_sha256=_RECOVERY_PROJECT_HASH,
|
|
init_ready=True,
|
|
status_message_sha256="1" * 64,
|
|
mqtt_retained=False,
|
|
observed_at_utc="2026-08-09T12:10:01.000Z",
|
|
)
|
|
)
|
|
|
|
original_request_stop = control.request_stop
|
|
|
|
def request_stop_with_durable_edge(
|
|
*,
|
|
confirmation: object,
|
|
command_context: object,
|
|
dispatch_admission_deadline_reached: Callable[[], bool] | None = None,
|
|
expected_session_generation: int | None = None,
|
|
expected_state_revision: int | None = None,
|
|
) -> dict[str, object]:
|
|
assert dispatch_admission_deadline_reached is not None
|
|
payload = b"recovered-stop"
|
|
coordinator.prepare(
|
|
command_context, # type: ignore[arg-type]
|
|
action="stop",
|
|
envelope=OneShotPublishEnvelope(
|
|
operation_key="modeling:stop",
|
|
topic="lixel/application/request/modeling",
|
|
payload=payload,
|
|
payload_sha256=hashlib.sha256(payload).hexdigest(),
|
|
payload_bytes=len(payload),
|
|
qos=2,
|
|
retain=False,
|
|
),
|
|
)
|
|
published_physical_edges.append("stop")
|
|
return original_request_stop(
|
|
confirmation=confirmation,
|
|
command_context=command_context,
|
|
dispatch_admission_deadline_reached=(
|
|
dispatch_admission_deadline_reached
|
|
),
|
|
expected_session_generation=expected_session_generation,
|
|
expected_state_revision=expected_state_revision,
|
|
)
|
|
|
|
control.request_stop = request_stop_with_durable_edge # type: ignore[method-assign]
|
|
bound_service._acquire_application_control_process_lease() # noqa: SLF001
|
|
|
|
restarted._bootstrap_prestart_control_ready_owned = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
bootstrap_recovery_control,
|
|
restarted,
|
|
)
|
|
verify_operation_id = "op-00000000-0000-4000-8000-000000001230"
|
|
verified = asyncio.run(
|
|
restarted.verify_connection(
|
|
ConnectionVerifyRequest(
|
|
device_id="test-ble-transport",
|
|
compatibility_attestation=ATTESTATION,
|
|
operation_id=verify_operation_id,
|
|
expected_discovery_generation=0,
|
|
)
|
|
)
|
|
)
|
|
|
|
acquisition = verified["acquisition"]
|
|
assert acquisition["acquisition_id"] == _RECOVERY_ACQUISITION_ID
|
|
assert acquisition["device_session_id"] == verified["device_session"]["device_session_id"]
|
|
assert acquisition["state"] == "failed"
|
|
assert acquisition["cleanup_pending"] is False
|
|
assert acquisition["requested_streams"] == []
|
|
assert acquisition["project_name"] is None
|
|
assert acquisition["result"] == {
|
|
"receiver_stopped": True,
|
|
"device_state": "scanning",
|
|
"recovery_only": True,
|
|
"physical_command_operation_id": _RECOVERY_START_OPERATION_ID,
|
|
"physical_reconciliation_id": f"{verify_operation_id}.physical",
|
|
"project_id_sha256": _RECOVERY_PROJECT_HASH,
|
|
"automatic_replay_allowed": False,
|
|
}
|
|
assert verified["application_control_session"]["state"] == "scanning"
|
|
assert verified["application_control_session"]["can_stop"] is True
|
|
assert published_physical_edges == []
|
|
|
|
control = restarted._application_control_session # noqa: SLF001
|
|
control_snapshot = control.snapshot()
|
|
with pytest.raises(RuntimeError, match="ожидать сохранения проекта"):
|
|
restarted.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="must-not-prepare",
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_control_session_generation=control_snapshot["session_generation"],
|
|
expected_control_state_revision=control_snapshot["state_revision"],
|
|
)
|
|
)
|
|
with pytest.raises(RuntimeError, match="ещё не готов принять START"):
|
|
restarted.start_acquisition(
|
|
_start_request(
|
|
acquisition_id=_RECOVERY_ACQUISITION_ID,
|
|
expected_state_revision=acquisition["state_revision"],
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
expected_control_session_generation=control_snapshot["session_generation"],
|
|
expected_control_state_revision=control_snapshot["state_revision"],
|
|
)
|
|
)
|
|
assert published_physical_edges == []
|
|
|
|
stop_operation_id = "op-00000000-0000-4000-8000-000000001231"
|
|
stop_request = _stop_request(
|
|
acquisition_id=_RECOVERY_ACQUISITION_ID,
|
|
operation_id=stop_operation_id,
|
|
idempotency_key="recovered-stop-once",
|
|
mode="graceful",
|
|
expected_control_session_generation=control_snapshot["session_generation"],
|
|
expected_control_state_revision=control_snapshot["state_revision"],
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
stopped = restarted.stop_acquisition(stop_request)
|
|
assert stopped["acquisition"]["result"]["recovery_only"] is True
|
|
assert published_physical_edges == ["stop"]
|
|
repeated = restarted.stop_acquisition(stop_request)
|
|
assert repeated["last_operation"]["operation_id"] == stop_operation_id
|
|
assert published_physical_edges == ["stop"]
|
|
|
|
payload_hash = hashlib.sha256(b"recovered-stop").hexdigest()
|
|
dispatch = ApplicationMqttPublishEvidence(
|
|
operation_key="modeling:stop",
|
|
topic="lixel/application/request/modeling",
|
|
payload_sha256=payload_hash,
|
|
qos=2,
|
|
retain=False,
|
|
packet_id=None,
|
|
)
|
|
coordinator.publish_dispatching(dispatch)
|
|
returned = ApplicationMqttPublishEvidence(
|
|
operation_key="modeling:stop",
|
|
topic="lixel/application/request/modeling",
|
|
payload_sha256=payload_hash,
|
|
qos=2,
|
|
retain=False,
|
|
packet_id=42,
|
|
)
|
|
coordinator.publish_result(returned, publish_call_returned=True)
|
|
coordinator.qos2_completed(returned)
|
|
coordinator.application_response(
|
|
ApplicationMqttResponseEvidence(
|
|
operation_key="modeling:stop",
|
|
response_topic="lixel/application/response/modeling",
|
|
payload_sha256="2" * 64,
|
|
modeling_action="stop",
|
|
result_code=PHYSICAL_COMMAND_APPLICATION_SUCCESS_CODE,
|
|
success=True,
|
|
observed_at_utc="2026-08-09T12:10:02.000Z",
|
|
)
|
|
)
|
|
coordinator.device_status(
|
|
ApplicationMqttDeviceStatusEvidence(
|
|
vendor_device_id_sha256=_RECOVERY_VENDOR_HASH,
|
|
device_serial_sha256=_RECOVERY_SERIAL_HASH,
|
|
session_state="ready",
|
|
session_state_code=MODELING_STATE_BASE + 300,
|
|
project_bound=False,
|
|
project_id_sha256=None,
|
|
init_ready=False,
|
|
status_message_sha256="3" * 64,
|
|
mqtt_retained=False,
|
|
observed_at_utc="2026-08-09T12:10:03.000Z",
|
|
)
|
|
)
|
|
coordinator.resolve("stop")
|
|
control.state = "completed"
|
|
ready = restarted.state()
|
|
|
|
assert ready["physical_command"]["record"]["resolution"] == "stop-standby-observed"
|
|
assert ready["physical_command"]["resolved_active_recovery_required"] is False
|
|
assert ready["connection_lifecycle"]["mode_selection"]["allowed"] is True
|
|
terminal_stop = next(
|
|
operation
|
|
for operation in ready["operations"]
|
|
if operation["operation_id"] == stop_operation_id
|
|
)
|
|
assert terminal_stop["status"] == "succeeded"
|
|
assert published_physical_edges == ["stop"]
|
|
|
|
|
|
def test_active_reconciliation_survives_post_commit_adoption_failure_and_next_ready_verify(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
coordinator = _VerifyPhysicalRecoveryCoordinator(
|
|
observed_session_state="scanning",
|
|
reconciliation_ready=True,
|
|
)
|
|
_install_synthetic_verify_recovery(service, coordinator)
|
|
bootstrap_attempts = 0
|
|
|
|
async def bootstrap_with_first_adoption_failure(
|
|
bound_service: XgridsK1CompatibilityService,
|
|
*,
|
|
parent_operation_id: str | None,
|
|
connection_mode: facade_module.ConnectionMode,
|
|
inspection_only: bool = False,
|
|
) -> None:
|
|
nonlocal bootstrap_attempts
|
|
bootstrap_attempts += 1
|
|
await _synthetic_prestart_control_bootstrap(
|
|
bound_service,
|
|
parent_operation_id=parent_operation_id,
|
|
connection_mode=connection_mode,
|
|
inspection_only=inspection_only,
|
|
)
|
|
bound_service._acquire_application_control_process_lease() # noqa: SLF001
|
|
if bootstrap_attempts == 1:
|
|
control = bound_service._application_control_session # noqa: SLF001
|
|
|
|
def fail_after_durable_commit(*_args: object, **_kwargs: object) -> None:
|
|
raise RuntimeError("synthetic adoption loss after ledger commit")
|
|
|
|
control.adopt_reconciled_scanning = fail_after_durable_commit # type: ignore[method-assign]
|
|
|
|
service._bootstrap_prestart_control_ready_owned = MethodType( # type: ignore[method-assign] # noqa: SLF001
|
|
bootstrap_with_first_adoption_failure,
|
|
service,
|
|
)
|
|
first_operation = "op-00000000-0000-4000-8000-000000001210"
|
|
|
|
with pytest.raises(facade_module.ConnectionVerificationError):
|
|
asyncio.run(
|
|
service.verify_connection(
|
|
_retained_physical_recovery_verify_request(operation_id=first_operation)
|
|
)
|
|
)
|
|
|
|
after_failure = service.state()
|
|
assert coordinator.resolution == "physical-active-observed"
|
|
assert after_failure["physical_command"]["resolved_active_recovery_required"] is True
|
|
assert after_failure["application_control_session"]["state"] == "idle"
|
|
mutation_reasons = after_failure["connection_policy"]["actions"][
|
|
"provision-fresh-device"
|
|
]["reason_codes"]
|
|
assert "physical-command-reconciliation-required" in mutation_reasons
|
|
assert "physical-device-already-active" in mutation_reasons
|
|
assert after_failure["connection_policy"]["actions"]["verify-control-device-info"][
|
|
"allowed"
|
|
] is True
|
|
|
|
coordinator.observed_session_state = "ready"
|
|
second_operation = "op-00000000-0000-4000-8000-000000001211"
|
|
verify_result = asyncio.run(
|
|
service.verify_connection(
|
|
_retained_physical_recovery_verify_request(operation_id=second_operation)
|
|
)
|
|
)
|
|
recovered = service.state()
|
|
|
|
assert coordinator.reconciliation_ids == [
|
|
f"{first_operation}.physical",
|
|
f"{second_operation}.physical",
|
|
]
|
|
assert coordinator.resolution == "physical-standby-observed"
|
|
assert verify_result["last_operation"]["status"] == "succeeded"
|
|
assert recovered["physical_command"]["resolved_active_recovery_required"] is False
|
|
assert recovered["physical_command"]["requires_reconciliation"] is False
|
|
assert recovered["connection_lifecycle"]["mode_selection"]["allowed"] is True, recovered[
|
|
"connection_lifecycle"
|
|
]["mode_selection"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("proof_kind", "observed_session_state"),
|
|
[
|
|
("missing-status", None),
|
|
("retained-status", "ready"),
|
|
("stale-device-info", None),
|
|
],
|
|
)
|
|
def test_explicit_verify_fails_closed_without_fresh_physical_recovery_proof(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
proof_kind: str,
|
|
observed_session_state: str | None,
|
|
) -> None:
|
|
del proof_kind
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
coordinator = _VerifyPhysicalRecoveryCoordinator(
|
|
observed_session_state=observed_session_state,
|
|
reconciliation_ready=False,
|
|
)
|
|
_install_synthetic_verify_recovery(service, coordinator)
|
|
monkeypatch.setattr(
|
|
facade_module,
|
|
"PHYSICAL_RECONCILIATION_PROOF_TIMEOUT_SECONDS",
|
|
0.0,
|
|
)
|
|
operation_id = "op-00000000-0000-4000-8000-000000001203"
|
|
|
|
with pytest.raises(facade_module.ConnectionVerificationError) as raised:
|
|
asyncio.run(
|
|
service.verify_connection(
|
|
_retained_physical_recovery_verify_request(operation_id=operation_id)
|
|
)
|
|
)
|
|
|
|
assert raised.value.reason_code == "physical-command-reconciliation-proof-timeout"
|
|
assert coordinator.reconciliation_ids == []
|
|
state = service.state()
|
|
operation = next(item for item in state["operations"] if item["operation_id"] == operation_id)
|
|
assert operation["status"] == "failed"
|
|
assert operation["stage_code"] == "physical-reconciliation-failed"
|
|
assert operation["error"]["code"] == ("physical-command-reconciliation-proof-timeout")
|
|
assert operation["error"]["safe_to_retry"] is True
|
|
assert operation["error"]["side_effect_status"] == "none"
|
|
assert state["application_control_session"]["state"] == "idle"
|
|
assert service._application_control_process_lease_holders == set() # noqa: SLF001
|
|
assert state["connection_policy"]["actions"]["verify-control-device-info"]["allowed"] is True
|
|
|
|
|
|
def test_bootstrap_identity_rejection_retires_only_session_opened_by_attempt(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Rejected DeviceInfo cannot leak a ready socket or process lease."""
|
|
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
control = FakeInteractiveControlSession(initial_state="idle")
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
_seed_supervised_connection(
|
|
service,
|
|
connection_mode="bridge",
|
|
transport_ref="k1-a",
|
|
with_control=False,
|
|
)
|
|
physical_edges: list[str] = []
|
|
|
|
def reject_identity(**_: object) -> None:
|
|
raise DeviceIdentityPinStoreCorrupt("synthetic DeviceInfo pin rejection")
|
|
|
|
monkeypatch.setattr(service, "_pin_or_match_device_identity", reject_identity)
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"prepare",
|
|
lambda *_args, **_kwargs: physical_edges.append("physical-command"),
|
|
)
|
|
|
|
with pytest.raises(facade_module.ConnectionVerificationError) as rejected:
|
|
asyncio.run(
|
|
XgridsK1CompatibilityService._bootstrap_prestart_control_ready_owned( # noqa: SLF001
|
|
service,
|
|
parent_operation_id=None,
|
|
connection_mode="bridge",
|
|
)
|
|
)
|
|
|
|
assert rejected.value.reason_code == "control-bootstrap-device-identity-unverified"
|
|
assert physical_edges == []
|
|
state = service.state()
|
|
assert state["application_control_session"]["state"] == "idle"
|
|
assert service._application_control_process_lease_holders == set() # noqa: SLF001
|
|
assert state["active_connection_mode"] is None
|
|
assert state["connection_policy"]["actions"]["scan-ble"]["allowed"] is True
|
|
assert state["connection_policy"]["actions"]["verify-control-device-info"]["allowed"] is True
|
|
|
|
|
|
@pytest.mark.parametrize("inspection_only", [True, False], ids=["verify", "ordinary"])
|
|
def test_read_only_bootstrap_detaches_physical_observer_before_fresh_control_open(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
inspection_only: bool,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
control = FakeInteractiveControlSession(initial_state="idle")
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
_seed_supervised_connection(
|
|
service,
|
|
connection_mode="bridge",
|
|
transport_ref="k1-a",
|
|
with_control=False,
|
|
)
|
|
ordering: list[str] = []
|
|
|
|
def detach_before_read_only_open() -> None:
|
|
ordering.append("physical-observer-detached")
|
|
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"prepare_read_only_bootstrap",
|
|
detach_before_read_only_open,
|
|
)
|
|
original_open = control.open
|
|
|
|
def open_after_detach(
|
|
*,
|
|
connection_binding: ApplicationConnectionBinding,
|
|
**kwargs: object,
|
|
) -> dict[str, object]:
|
|
if inspection_only:
|
|
assert ordering == ["physical-observer-detached"]
|
|
else:
|
|
assert ordering == []
|
|
ordering.append("fresh-control-observer-opened")
|
|
return original_open(connection_binding=connection_binding, **kwargs)
|
|
|
|
monkeypatch.setattr(control, "open", open_after_detach)
|
|
|
|
asyncio.run(
|
|
XgridsK1CompatibilityService._bootstrap_prestart_control_ready_owned( # noqa: SLF001
|
|
service,
|
|
parent_operation_id=None,
|
|
connection_mode="bridge",
|
|
inspection_only=inspection_only,
|
|
)
|
|
)
|
|
|
|
assert ordering == (
|
|
["physical-observer-detached", "fresh-control-observer-opened"]
|
|
if inspection_only
|
|
else ["fresh-control-observer-opened"]
|
|
)
|
|
assert control.state == "connection-ready"
|
|
|
|
|
|
def test_polling_self_cleans_prepared_acquisition_after_proven_prestart_failure(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""A battery/control loss before START recovers without refresh or ABORT."""
|
|
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
_select_connection_mode(service, "quick-connect")
|
|
|
|
class TerminalPrestartControl(FakeInteractiveControlSession):
|
|
failure: dict[str, object] | None = None
|
|
|
|
def snapshot(self) -> dict[str, object]:
|
|
snapshot = super().snapshot()
|
|
snapshot["failure"] = self.failure
|
|
return snapshot
|
|
|
|
control = TerminalPrestartControl()
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
binding = _seed_supervised_connection(
|
|
service,
|
|
target_ipv4=facade_module.AP_FALLBACK_IPV4,
|
|
connection_mode="quick-connect",
|
|
transport_ref="k1-a",
|
|
)
|
|
control.verified_control = _verified_control_for_binding(binding)
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="TEST001",
|
|
host=facade_module.AP_FALLBACK_IPV4,
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
)
|
|
)
|
|
assert prepared["acquisition"]["state"] == "prepared"
|
|
service._acquire_application_control_process_lease() # noqa: SLF001
|
|
physical_or_network_edges: list[str] = []
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"prepare",
|
|
lambda *_args, **_kwargs: physical_or_network_edges.append("physical-command"),
|
|
)
|
|
|
|
async def forbidden_device_write(*_: object, **__: object) -> dict[str, Any]:
|
|
physical_or_network_edges.append("network-write")
|
|
raise AssertionError("polling recovery must remain read/local-only")
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_device_write)
|
|
control.state = "failed"
|
|
control.failure = {
|
|
"reason_code": "mqtt_network_loop_failed",
|
|
"modeling_command_attempted": False,
|
|
"safe_to_retry": True,
|
|
"network_change_admissible": True,
|
|
}
|
|
recovered = service.state()
|
|
|
|
assert physical_or_network_edges == []
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
assert recovered["acquisition"]["state"] == "aborted"
|
|
assert recovered["acquisition"]["result"] == {
|
|
"receiver_started": False,
|
|
"device_command_attempted": False,
|
|
"reason_code": "prestart-control-terminal-failure",
|
|
}
|
|
assert recovered["application_control_session"]["state"] == "idle"
|
|
assert service._application_control_process_lease_holders == set() # noqa: SLF001
|
|
assert recovered["connection_policy"]["actions"]["scan-ble"]["allowed"] is True
|
|
|
|
|
|
@pytest.mark.parametrize("observed_session_state", ["ready", "scanning"])
|
|
def test_verify_classification_settles_original_prepared_stop_as_not_dispatched(
|
|
tmp_path: Path,
|
|
observed_session_state: str,
|
|
) -> None:
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
original_stop_operation_id = "op-00000000-0000-4000-8000-000000001409"
|
|
operation, created = service._operations.begin( # noqa: SLF001
|
|
facade_module.ACTION_ACQUISITION_STOP,
|
|
operation_id=original_stop_operation_id,
|
|
device_id="known-k1",
|
|
device_session_id="old-stop-session",
|
|
deadline_seconds=60.0,
|
|
idempotency_key="old-prepared-stop",
|
|
)
|
|
assert created is True
|
|
service._operations.transition( # noqa: SLF001
|
|
operation.operation_id,
|
|
"running",
|
|
stage_code="awaiting-external-stop",
|
|
message_code="acquisition.stop.device_stopping",
|
|
)
|
|
coordinator = _VerifyPhysicalRecoveryCoordinator(
|
|
observed_session_state=observed_session_state,
|
|
reconciliation_ready=True,
|
|
prepared_stop_operation_id=original_stop_operation_id,
|
|
)
|
|
_install_synthetic_verify_recovery(service, coordinator)
|
|
verify_operation_id = "op-00000000-0000-4000-8000-000000001410"
|
|
|
|
verified = asyncio.run(
|
|
service.verify_connection(
|
|
_retained_physical_recovery_verify_request(operation_id=verify_operation_id)
|
|
)
|
|
)
|
|
|
|
original = service._operations.get(original_stop_operation_id) # noqa: SLF001
|
|
assert original.status == "failed"
|
|
assert original.stage_code == "physical-stop-classified-not-dispatched"
|
|
assert original.error == {
|
|
"category": "device",
|
|
"code": "physical-stop-not-dispatched",
|
|
"retryable": False,
|
|
"safe_to_retry": False,
|
|
"side_effect_status": "none",
|
|
"physical_command_sent": False,
|
|
"automatic_replay_allowed": False,
|
|
}
|
|
assert coordinator._resolved_record(f"{verify_operation_id}.physical")["resolution"] == ( # noqa: SLF001
|
|
"not-dispatched"
|
|
)
|
|
if observed_session_state == "scanning":
|
|
assert verified["acquisition"]["result"]["physical_command_operation_id"] == (
|
|
original_stop_operation_id
|
|
)
|
|
assert verified["application_control_session"]["can_stop"] is True
|
|
else:
|
|
assert verified["application_control_session"]["can_stop"] is False
|
|
acquisition = verified.get("acquisition")
|
|
assert not (
|
|
isinstance(acquisition, dict)
|
|
and isinstance(acquisition.get("result"), dict)
|
|
and acquisition["result"].get("recovery_only") is True
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("observed_session_state", ["ready", "scanning"])
|
|
def test_public_verify_reconciles_fresh_service_resolved_unclassified_stop_without_writes(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
observed_session_state: str,
|
|
) -> None:
|
|
original, _ = service_with_fake_runtime(tmp_path)
|
|
stop_operation_id, compatibility_revision = (
|
|
_persist_resolved_unclassified_stop_for_restart(original)
|
|
)
|
|
|
|
restarted, runtime = service_with_fake_runtime(tmp_path)
|
|
before = restarted._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
assert before is not None
|
|
assert before.operation_id == stop_operation_id
|
|
assert before.revision == compatibility_revision
|
|
assert before.stage == "resolved"
|
|
assert before.resolution == "not-dispatched"
|
|
assert before.resolved_unclassified_stop_recovery_required is True
|
|
assert restarted._acquisition is None # noqa: SLF001
|
|
|
|
forbidden_io: list[str] = []
|
|
|
|
async def forbid_device_io(*_args: object, **_kwargs: object) -> object:
|
|
forbidden_io.append("device-io")
|
|
raise AssertionError("resolved STOP Verify must stay read-only")
|
|
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbid_device_io)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbid_device_io)
|
|
_install_real_coordinator_verify_recovery(
|
|
restarted,
|
|
observed_session_state=observed_session_state,
|
|
)
|
|
verify_operation_id = "op-00000000-0000-4000-8000-000000001411"
|
|
|
|
verified = asyncio.run(
|
|
restarted.verify_connection(
|
|
_retained_physical_recovery_verify_request(
|
|
operation_id=verify_operation_id,
|
|
)
|
|
)
|
|
)
|
|
|
|
after = restarted._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
assert after is not None
|
|
assert after.operation_id == stop_operation_id
|
|
assert after.revision == compatibility_revision + 1
|
|
assert after.stage == "resolved"
|
|
assert after.resolution == "not-dispatched"
|
|
assert after.resolved_unclassified_stop_recovery_required is False
|
|
classification = after.reconciliations[-1]
|
|
assert classification.kind == "prepared-stop-classification"
|
|
assert classification.original_attempt.stage == "resolved"
|
|
assert classification.original_attempt.resolution == "not-dispatched"
|
|
assert classification.resolution == (
|
|
"physical-active-observed"
|
|
if observed_session_state == "scanning"
|
|
else "physical-standby-observed"
|
|
)
|
|
assert verified["last_operation"]["operation_id"] == verify_operation_id
|
|
assert verified["last_operation"]["status"] == "succeeded"
|
|
assert verified["last_operation"]["result"]["write_performed"] is False
|
|
assert verified["last_operation"]["result"]["physical_reconciliation"] == {
|
|
"performed": True,
|
|
"resolution": classification.resolution,
|
|
"observed_session_state": observed_session_state,
|
|
"device_write_performed": False,
|
|
"automatic_retry": False,
|
|
}
|
|
control = restarted._application_control_session # noqa: SLF001
|
|
assert isinstance(control, FakeInteractiveControlSession)
|
|
assert control.start_projects == []
|
|
assert control.stop_calls == 0
|
|
assert runtime.start_calls == []
|
|
assert runtime.stop_calls == 0
|
|
assert forbidden_io == []
|
|
|
|
if observed_session_state == "ready":
|
|
assert verified.get("acquisition") is None
|
|
assert verified["application_control_session"]["state"] == "connection-ready"
|
|
assert verified["application_control_session"]["can_stop"] is False
|
|
assert verified["connection_policy"]["actions"]["start-acquisition"][
|
|
"allowed"
|
|
] is True
|
|
assert "physical-command-reconciliation-required" not in verified[
|
|
"connection_policy"
|
|
]["actions"]["start-acquisition"]["reason_codes"]
|
|
assert verified["connection_lifecycle"]["mode_selection"]["allowed"] is False
|
|
with pytest.raises(facade_module.NetworkProvisioningConflict) as mutation:
|
|
restarted._require_physical_command_network_mutation_allowed() # noqa: SLF001
|
|
assert mutation.value.reason_code == "physical-command-reconciliation-required"
|
|
else:
|
|
assert verified["application_control_session"]["state"] == "scanning"
|
|
assert verified["application_control_session"]["can_stop"] is True
|
|
assert verified["acquisition"]["result"]["recovery_only"] is True
|
|
assert verified["acquisition"]["result"]["physical_command_operation_id"] == (
|
|
stop_operation_id
|
|
)
|
|
assert verified["connection_policy"]["actions"]["start-acquisition"][
|
|
"allowed"
|
|
] is False
|
|
assert verified["connection_policy"]["actions"]["stop-acquisition"][
|
|
"allowed"
|
|
] is True
|
|
|
|
|
|
def test_restart_classified_ready_uses_fresh_live_binding_for_one_start(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""C1 proves historical standby; fresh DeviceInfo + live READY on C2 owns START."""
|
|
|
|
original, _ = service_with_fake_runtime(tmp_path)
|
|
stop_operation_id, c1 = _persist_classified_ready_stop_for_restart(original)
|
|
classified = original._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
assert classified is not None
|
|
assert classified.operation_id == stop_operation_id
|
|
assert classified.reconciliations[-1].verified_binding.connection == c1
|
|
|
|
restarted, runtime = service_with_fake_runtime(tmp_path)
|
|
binding = _seed_supervised_connection(
|
|
restarted,
|
|
target_ipv4="192.168.68.52",
|
|
connection_mode="bridge",
|
|
transport_ref="test-ble-transport",
|
|
logical_device_id="known-k1",
|
|
)
|
|
stable_path = _direct_host_path(binding.target_ipv4)
|
|
monkeypatch.setattr(
|
|
restarted,
|
|
"_sample_host_path",
|
|
lambda *_args, **_kwargs: stable_path,
|
|
)
|
|
control = _BindingValidatingControlSession(restarted, binding)
|
|
control.open(connection_binding=binding)
|
|
control.verified_control = _verified_control_for_binding(
|
|
binding,
|
|
logical_device_id="known-k1",
|
|
control_session_id="fresh-live-c2-control",
|
|
control_proof_revision=2,
|
|
)
|
|
restarted._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
restarted._reconcile_connection_supervisor( # noqa: SLF001
|
|
control.snapshot(),
|
|
runtime.snapshot(),
|
|
)
|
|
restarted._acquire_application_control_process_lease() # noqa: SLF001
|
|
|
|
coordinator = restarted._physical_command_coordinator # noqa: SLF001
|
|
observed_at_utc = datetime.now(UTC).isoformat(timespec="milliseconds").replace(
|
|
"+00:00",
|
|
"Z",
|
|
)
|
|
coordinator.application_response(
|
|
ApplicationMqttResponseEvidence(
|
|
operation_key="bootstrap:fresh-live-c2:DeviceInfoRequest",
|
|
response_topic="lixel/application/response/device_info",
|
|
payload_sha256="6" * 64,
|
|
modeling_action=None,
|
|
result_code=None,
|
|
success=None,
|
|
observed_at_utc=observed_at_utc,
|
|
)
|
|
)
|
|
verified = control.verified_control
|
|
assert verified is not None
|
|
c2 = PhysicalCommandRuntimeBinding(
|
|
vendor_device_id_sha256=_RECOVERY_VENDOR_HASH,
|
|
device_serial_sha256=_RECOVERY_SERIAL_HASH,
|
|
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
|
|
intent_id=str(verified["intent_id"]),
|
|
transport_ref=str(verified["transport_ref"]),
|
|
connection_mode=verified["connection_mode"], # type: ignore[arg-type]
|
|
target_ipv4=str(verified["target_ipv4"]),
|
|
target_port=int(verified["target_port"]),
|
|
host_path_epoch=int(verified["host_path_epoch"]),
|
|
control_session_id=str(verified["control_session_id"]),
|
|
producer_generation=int(verified["producer_generation"]),
|
|
)
|
|
assert c2.control_session_id != c1.control_session_id
|
|
coordinator.bind_control_session(c2)
|
|
coordinator.device_status(
|
|
ApplicationMqttDeviceStatusEvidence(
|
|
vendor_device_id_sha256=_RECOVERY_VENDOR_HASH,
|
|
device_serial_sha256=_RECOVERY_SERIAL_HASH,
|
|
session_state="ready",
|
|
session_state_code=MODELING_STATE_BASE + 300,
|
|
project_bound=False,
|
|
project_id_sha256=None,
|
|
init_ready=False,
|
|
status_message_sha256="7" * 64,
|
|
mqtt_retained=False,
|
|
observed_at_utc=observed_at_utc,
|
|
)
|
|
)
|
|
|
|
inherited_reconciliations = classified.reconciliations
|
|
ready = restarted.state()
|
|
workspace = restarted.enter_application_workspace(
|
|
EnterApplicationWorkspaceRequest(
|
|
operator_confirmed=True,
|
|
expected_session_generation=ready["application_control_session"][
|
|
"session_generation"
|
|
],
|
|
expected_state_revision=ready["application_control_session"]["state_revision"],
|
|
)
|
|
)
|
|
prepared = restarted.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="RESTART_READY_C2",
|
|
host=binding.target_ipv4,
|
|
compatibility_attestation=ATTESTATION,
|
|
expected_control_session_generation=workspace["application_control_session"][
|
|
"session_generation"
|
|
],
|
|
expected_control_state_revision=workspace["application_control_session"][
|
|
"state_revision"
|
|
],
|
|
)
|
|
)
|
|
before_start = restarted._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
assert before_start == classified
|
|
assert before_start.reconciliations == inherited_reconciliations
|
|
|
|
prepared_edges: list[str] = []
|
|
original_request_start = control.request_start
|
|
|
|
def request_start_with_durable_edge(
|
|
*,
|
|
project_name: str,
|
|
confirmation: object,
|
|
command_context: object,
|
|
preparation_checkpoint_observer: object | None = None,
|
|
expected_session_generation: int | None = None,
|
|
expected_state_revision: int | None = None,
|
|
) -> dict[str, object]:
|
|
payload = b"fresh-start-on-c2"
|
|
coordinator.prepare(
|
|
command_context, # type: ignore[arg-type]
|
|
action="start",
|
|
envelope=OneShotPublishEnvelope(
|
|
operation_key="modeling:start",
|
|
topic="lixel/application/request/modeling",
|
|
payload=payload,
|
|
payload_sha256=hashlib.sha256(payload).hexdigest(),
|
|
payload_bytes=len(payload),
|
|
qos=2,
|
|
retain=False,
|
|
),
|
|
)
|
|
prepared_edges.append("start")
|
|
return original_request_start(
|
|
project_name=project_name,
|
|
confirmation=confirmation,
|
|
command_context=command_context,
|
|
preparation_checkpoint_observer=preparation_checkpoint_observer,
|
|
expected_session_generation=expected_session_generation,
|
|
expected_state_revision=expected_state_revision,
|
|
)
|
|
|
|
control.request_start = request_start_with_durable_edge # type: ignore[method-assign]
|
|
start_operation_id = "op-00000000-0000-4000-8000-000000001412"
|
|
started = restarted.start_acquisition(
|
|
_start_request(
|
|
acquisition_id=prepared["acquisition"]["acquisition_id"],
|
|
operation_id=start_operation_id,
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
expected_control_session_generation=prepared["application_control_session"][
|
|
"session_generation"
|
|
],
|
|
expected_control_state_revision=prepared["application_control_session"][
|
|
"state_revision"
|
|
],
|
|
)
|
|
)
|
|
|
|
successor = restarted._physical_command_ledger.snapshot().record # noqa: SLF001
|
|
assert successor is not None
|
|
assert successor.operation_id == start_operation_id
|
|
assert successor.parent_operation_id == stop_operation_id
|
|
assert successor.action == "start"
|
|
assert successor.stage == "prepared"
|
|
assert successor.connection.control_session_id == c2.control_session_id
|
|
assert successor.connection.host_path_epoch == c2.host_path_epoch
|
|
assert successor.connection.producer_generation == c2.producer_generation
|
|
assert successor.baseline_status.control_session_id == c2.control_session_id
|
|
assert successor.reconciliations == inherited_reconciliations
|
|
assert prepared_edges == ["start"]
|
|
assert control.start_projects == ["RESTART_READY_C2"]
|
|
assert len(runtime.start_calls) == 1
|
|
assert started["acquisition"]["state"] == "starting"
|
|
|
|
|
|
def test_recovered_terminal_stop_losing_wifi_stays_acknowledged_unconfirmed_and_never_replays(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
|
|
class TerminalStopControl(FakeInteractiveControlSession):
|
|
failure: dict[str, object] | None = None
|
|
|
|
def snapshot(self) -> dict[str, object]:
|
|
snapshot = super().snapshot()
|
|
snapshot["failure"] = self.failure
|
|
return snapshot
|
|
|
|
control = TerminalStopControl(initial_state="workspace-ready")
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
binding = _seed_supervised_connection(
|
|
service,
|
|
connection_mode="bridge",
|
|
transport_ref="k1-a",
|
|
)
|
|
control.verified_control = _verified_control_for_binding(binding)
|
|
prepared = service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="RECOVERED_STOP",
|
|
host="192.168.1.20",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
acquisition_id = prepared["acquisition"]["acquisition_id"]
|
|
service.start_acquisition(
|
|
_start_request(
|
|
acquisition_id=acquisition_id,
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
)
|
|
start_operation_id = service._acquisition_start_operation_id # noqa: SLF001
|
|
assert isinstance(start_operation_id, str)
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"snapshot",
|
|
lambda: _exact_start_physical_proof(
|
|
operation_id=start_operation_id,
|
|
acquisition_id=acquisition_id,
|
|
binding=binding,
|
|
resolved=True,
|
|
),
|
|
)
|
|
runtime.mark_ready()
|
|
runtime.pcl_frames = 1
|
|
acquiring = service.state()
|
|
assert acquiring["acquisition"]["state"] == "acquiring"
|
|
|
|
# Model the already-sealed local half after power loss. A later read-only
|
|
# Verify has adopted SCANNING, so the terminal acquisition exposes one
|
|
# explicit canonical STOP without restarting the receiver or capture.
|
|
service._stop_acquisition_sources( # noqa: SLF001
|
|
camera_status="failed",
|
|
camera_failure_code="power-loss-before-recovery-stop",
|
|
)
|
|
with service._lock: # noqa: SLF001
|
|
assert service._acquisition is not None # noqa: SLF001
|
|
service._acquisition.transition( # noqa: SLF001
|
|
"failed",
|
|
message_code="acquisition.connection_lost",
|
|
result={"receiver_stopped": True, "device_state": "active-unconfirmed"},
|
|
)
|
|
control.state = "scanning"
|
|
stop_operation_id = "op-00000000-0000-4000-8000-000000001220"
|
|
stop_request = _stop_request(
|
|
acquisition_id=acquisition_id,
|
|
operation_id=stop_operation_id,
|
|
mode="graceful",
|
|
physical_acceptance=PHYSICAL_ACCEPTANCE,
|
|
)
|
|
|
|
issued = service.stop_acquisition(stop_request)
|
|
|
|
assert issued["acquisition"]["state"] == "failed"
|
|
assert control.stop_calls == 1
|
|
stop_operation = next(
|
|
operation
|
|
for operation in issued["operations"]
|
|
if operation["operation_id"] == stop_operation_id
|
|
)
|
|
assert stop_operation["status"] == "running"
|
|
|
|
control.state = "failed"
|
|
control.failure = {
|
|
"reason_code": "mqtt_network_loop_failed",
|
|
"safe_to_retry": False,
|
|
"modeling_command_attempted": True,
|
|
"network_change_admissible": True,
|
|
"network_change_reconciliation": {
|
|
"device_session_state": "scan_stopping",
|
|
"device_project_bound": True,
|
|
"system_error_code": None,
|
|
"stop_complete": True,
|
|
"standby_confirmed": False,
|
|
"automatic_retry": False,
|
|
},
|
|
}
|
|
physical_proof = {
|
|
"status": "unresolved",
|
|
"reason_code": "physical-command-reconciliation-required",
|
|
"requires_reconciliation": True,
|
|
"resolved_active_recovery_required": False,
|
|
"record": {
|
|
"operation_id": stop_operation_id,
|
|
"action": "stop",
|
|
"stage": "observing",
|
|
"resolution": None,
|
|
"application_response": {"action": "stop", "success": True},
|
|
"last_status": {
|
|
"session_state": "scan_stopping",
|
|
"mqtt_retained": False,
|
|
"project_bound": True,
|
|
"system_error_code": None,
|
|
},
|
|
"connection": {
|
|
"transport_ref": "k1-a",
|
|
"connection_mode": "bridge",
|
|
},
|
|
"reconciliations": [],
|
|
},
|
|
}
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"snapshot",
|
|
lambda: physical_proof,
|
|
)
|
|
|
|
recovered = service.state()
|
|
terminal_stop = next(
|
|
operation
|
|
for operation in recovered["operations"]
|
|
if operation["operation_id"] == stop_operation_id
|
|
)
|
|
|
|
assert terminal_stop["status"] == "failed"
|
|
assert terminal_stop["stage_code"] == "stop-acknowledged-standby-unconfirmed"
|
|
assert terminal_stop["error"] == {
|
|
"category": "device",
|
|
"code": "stop-acknowledged-standby-unconfirmed",
|
|
"retryable": False,
|
|
"safe_to_retry": False,
|
|
"side_effect_status": "unknown",
|
|
"automatic_replay_allowed": False,
|
|
}
|
|
assert recovered["application_control_session"]["state"] == "idle"
|
|
assert recovered["source_mode"] == "idle"
|
|
assert recovered["acquisition"]["cleanup_pending"] is False
|
|
assert control.stop_calls == 1
|
|
|
|
repeated = service.stop_acquisition(stop_request)
|
|
repeated_stop = next(
|
|
operation
|
|
for operation in repeated["operations"]
|
|
if operation["operation_id"] == stop_operation_id
|
|
)
|
|
assert repeated_stop["stage_code"] == "stop-acknowledged-standby-unconfirmed"
|
|
assert control.stop_calls == 1
|
|
|
|
|
|
def test_mode_select_winner_fences_concurrent_prepare_before_stale_record(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""PREPARE and Mode Select share one winner before local publication."""
|
|
|
|
service, runtime = service_with_fake_runtime(tmp_path)
|
|
_select_connection_mode(service, "quick-connect")
|
|
control = FakeInteractiveControlSession(initial_state="connection-ready")
|
|
service._application_control_session = control # type: ignore[assignment] # noqa: SLF001
|
|
binding = _seed_supervised_connection(
|
|
service,
|
|
target_ipv4=facade_module.AP_FALLBACK_IPV4,
|
|
connection_mode="quick-connect",
|
|
transport_ref="k1-a",
|
|
)
|
|
control.verified_control = _verified_control_for_binding(binding)
|
|
selection_entered = threading.Event()
|
|
release_selection = threading.Event()
|
|
selection_results: list[dict[str, Any]] = []
|
|
selection_failures: list[BaseException] = []
|
|
original_runtime_snapshot = runtime.snapshot
|
|
snapshot_pause_used = False
|
|
snapshot_pause_lock = threading.Lock()
|
|
|
|
def paused_selection_runtime_snapshot() -> dict[str, object]:
|
|
nonlocal snapshot_pause_used
|
|
should_pause = False
|
|
if threading.current_thread().name == "mode-select-winner":
|
|
with snapshot_pause_lock:
|
|
if not snapshot_pause_used:
|
|
snapshot_pause_used = True
|
|
should_pause = True
|
|
if should_pause:
|
|
selection_entered.set()
|
|
assert release_selection.wait(2.0)
|
|
return original_runtime_snapshot()
|
|
|
|
monkeypatch.setattr(runtime, "snapshot", paused_selection_runtime_snapshot)
|
|
|
|
def run_selection() -> None:
|
|
try:
|
|
selection_results.append(
|
|
service.select_connection_mode(
|
|
DesiredConnectionModeRequest(
|
|
connection_mode="bridge",
|
|
expected_revision=1,
|
|
)
|
|
)
|
|
)
|
|
except BaseException as exc: # pragma: no cover - assertion aid
|
|
selection_failures.append(exc)
|
|
|
|
selection_thread = threading.Thread(
|
|
target=run_selection,
|
|
name="mode-select-winner",
|
|
daemon=True,
|
|
)
|
|
selection_thread.start()
|
|
assert selection_entered.wait(2.0)
|
|
try:
|
|
with pytest.raises(facade_module.ApplicationControlProcessLeaseUnavailable):
|
|
service.prepare_acquisition(
|
|
_prepare_request(
|
|
project_name="STALE01",
|
|
host=facade_module.AP_FALLBACK_IPV4,
|
|
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
|
)
|
|
)
|
|
finally:
|
|
release_selection.set()
|
|
selection_thread.join(timeout=3.0)
|
|
|
|
assert selection_thread.is_alive() is False
|
|
assert selection_failures == []
|
|
assert selection_results[0]["desired_connection_mode"] == "bridge"
|
|
assert selection_results[0]["desired_connection_mode_revision"] == 2
|
|
assert service._acquisition is None # noqa: SLF001
|
|
assert control.state == "connection-ready"
|
|
device_edges: list[str] = []
|
|
|
|
async def fake_scan(
|
|
_duration_seconds: float,
|
|
*,
|
|
on_admitted: Callable[[], None] | None = None,
|
|
) -> dict[str, Any]:
|
|
assert service._acquisition is None # noqa: SLF001
|
|
assert control.state == "idle"
|
|
assert on_admitted is not None
|
|
on_admitted()
|
|
return _ble_scan_result("bridge-candidate")
|
|
|
|
async def forbidden_network_write(*_: object, **__: object) -> dict[str, Any]:
|
|
device_edges.append("network-write")
|
|
raise AssertionError("losing PREPARE must not create a device edge")
|
|
|
|
monkeypatch.setattr(facade_module, "scan", fake_scan)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_network_write)
|
|
monkeypatch.setattr(
|
|
service._physical_command_coordinator, # noqa: SLF001
|
|
"prepare",
|
|
lambda *_args, **_kwargs: device_edges.append("physical-command"),
|
|
)
|
|
|
|
scanned = asyncio.run(service.scan_ble(1.0))
|
|
|
|
assert device_edges == []
|
|
assert scanned["acquisition"] is None
|
|
assert scanned["desired_connection_mode"] == "bridge"
|
|
assert [item["device_id"] for item in scanned["devices"]] == ["bridge-candidate"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("network_stage", "expected_resolution", "expected_terminal_records"),
|
|
[
|
|
("prepared", "not-dispatched", 0),
|
|
("dispatching", "interrupted", 1),
|
|
("observing", "interrupted", 1),
|
|
],
|
|
ids=["prepared", "dispatching", "observing"],
|
|
)
|
|
def test_restart_after_any_open_network_stage_starts_with_a_clean_explicit_flow(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
network_stage: str,
|
|
expected_resolution: str,
|
|
expected_terminal_records: int,
|
|
) -> None:
|
|
"""CONN-08/11: restart terminalizes audit without restoring live authority."""
|
|
|
|
first, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_supervised_connection(
|
|
first,
|
|
target_ipv4="192.168.68.50",
|
|
connection_mode="bridge",
|
|
transport_ref="k1-a",
|
|
logical_device_id="device-a",
|
|
)
|
|
_set_scanned_k1(first, device_id="k1-a")
|
|
_seed_legacy_network_mutation_without_idempotency(
|
|
first,
|
|
operation_id=f"restart-{network_stage}-operation",
|
|
stage=network_stage,
|
|
)
|
|
assert first.state()["selected_device_id"] == "k1-a"
|
|
|
|
# A process restart loses every CoreBluetooth object even when the same
|
|
# macOS UUID is later advertised again. Construction/recovery is durable
|
|
# bookkeeping only and may not discover, read or write the device.
|
|
_SYNTHETIC_SCAN_CAPTURES.clear()
|
|
implicit_device_edges: list[str] = []
|
|
|
|
async def forbidden_scan(*_: object, **__: object) -> dict[str, Any]:
|
|
implicit_device_edges.append("scan")
|
|
raise AssertionError("restart recovery must not scan automatically")
|
|
|
|
async def forbidden_read(*_: object, **__: object) -> dict[str, Any]:
|
|
implicit_device_edges.append("read")
|
|
raise AssertionError("restart recovery must not open GATT automatically")
|
|
|
|
async def forbidden_write(*_: object, **__: object) -> dict[str, Any]:
|
|
implicit_device_edges.append("write")
|
|
raise AssertionError("restart recovery must not replay a network write")
|
|
|
|
monkeypatch.setattr(facade_module, "scan", forbidden_scan)
|
|
monkeypatch.setattr(facade_module, "read_wifi_status_once", forbidden_read)
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_write)
|
|
|
|
restarted, _ = service_with_fake_runtime(tmp_path)
|
|
state = restarted.state()
|
|
|
|
assert implicit_device_edges == []
|
|
assert state["devices"] == []
|
|
assert state["selected_device_id"] is None
|
|
assert state["device_session"] is None
|
|
assert state["current_device_recovery"] is None
|
|
assert state["configured_connection_mode"] is None
|
|
assert state["active_connection_mode"] is None
|
|
assert state["connection_lifecycle"]["active_binding"] is None
|
|
assert state["connection_lifecycle"]["connection_ready"] is False
|
|
assert state["network_write_reconciliation"] is None
|
|
assert state["network_mutation_ledger"]["status"] == "resolved"
|
|
assert state["network_mutation_ledger"]["resolution"] == expected_resolution
|
|
assert state["network_provisioning_idempotency"]["active_operation_id"] is None
|
|
assert state["network_provisioning_idempotency"]["terminal_record_count"] == (
|
|
expected_terminal_records
|
|
)
|
|
assert state["connection_policy"]["actions"]["scan-ble"]["allowed"] is True
|
|
assert state["connection_lifecycle"]["automatic_retry"] is False
|
|
|
|
# A later advertisement is only a fresh candidate. It does not resurrect
|
|
# the old binding, but it does make a new explicit Connect admissible.
|
|
_set_scanned_k1(restarted, device_id="k1-a")
|
|
fresh = restarted.state()
|
|
assert fresh["selected_device_id"] is None
|
|
assert fresh["active_connection_mode"] is None
|
|
assert fresh["connection_policy"]["actions"]["provision-fresh-device"]["allowed"] is True
|
|
assert implicit_device_edges == []
|
|
|
|
|
|
def test_power_loss_before_network_prepare_clears_selection_without_write_or_retry(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""CONN-10: a pre-PREPARED disconnect is a clean, retryable no-write end."""
|
|
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_seed_supervised_connection(
|
|
service,
|
|
target_ipv4=facade_module.AP_FALLBACK_IPV4,
|
|
connection_mode="quick-connect",
|
|
transport_ref="k1-a",
|
|
logical_device_id="device-a",
|
|
)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
provisioning_calls = 0
|
|
|
|
async def powered_off_before_prepare(
|
|
*_: object,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**__: object,
|
|
) -> dict[str, Any]:
|
|
nonlocal provisioning_calls
|
|
provisioning_calls += 1
|
|
assert on_write_dispatch is not None
|
|
error = RuntimeError("K1 powered off before durable prepare")
|
|
error.operation_stage = "gatt-connect" # type: ignore[attr-defined]
|
|
error.device_write_attempted = False # type: ignore[attr-defined]
|
|
error.device_write_confirmed = False # type: ignore[attr-defined]
|
|
raise error
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", powered_off_before_prepare)
|
|
|
|
with pytest.raises(RuntimeError, match="powered off before durable prepare"):
|
|
asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
connection_mode="bridge",
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
state = service.state()
|
|
operation = next(item for item in state["operations"] if item["action"] == "network.provision")
|
|
assert provisioning_calls == 1
|
|
assert state["selected_device_id"] is None
|
|
assert state["device_session"] is None
|
|
assert state["configured_connection_mode"] is None
|
|
assert state["active_connection_mode"] is None
|
|
assert state["network_mutation_ledger"]["status"] == "empty"
|
|
assert state["network_provisioning_idempotency"]["active_operation_id"] is None
|
|
assert operation["status"] == "failed"
|
|
assert operation["error"]["side_effect_status"] == "none"
|
|
assert operation["error"]["safe_to_retry"] is True
|
|
assert operation["context"]["automatic_retry"] is False
|
|
assert state["connection_attempt"]["phase"] == "network_not_applied"
|
|
assert state["connection_attempt"]["side_effect_status"] == "none"
|
|
assert state["connection_attempt"]["automatic_retry"] is False
|
|
assert state["connection_policy"]["actions"]["scan-ble"]["allowed"] is True
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
assert (
|
|
service.state()["connection_policy"]["actions"]["provision-fresh-device"]["allowed"] is True
|
|
)
|
|
assert provisioning_calls == 1
|
|
|
|
|
|
def test_power_loss_after_dispatch_becomes_terminal_unknown_without_replay(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""CONN-12: a dispatched write is unknown audit, never an automatic retry."""
|
|
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
write_calls = 0
|
|
|
|
async def powered_off_after_dispatch(
|
|
*_: object,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**__: object,
|
|
) -> dict[str, Any]:
|
|
nonlocal write_calls
|
|
write_calls += 1
|
|
_dispatch_test_network_write(on_write_dispatch)
|
|
error = RuntimeError("K1 powered off after dispatch")
|
|
error.operation_stage = "gatt-write" # type: ignore[attr-defined]
|
|
error.device_write_attempted = True # type: ignore[attr-defined]
|
|
error.device_write_confirmed = False # type: ignore[attr-defined]
|
|
raise error
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", powered_off_after_dispatch)
|
|
|
|
with pytest.raises(RuntimeError, match="powered off after dispatch"):
|
|
asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
# The old advertisement may already be gone after the failed write. The
|
|
# durable unresolved ledger still pins an exact read-only target and must
|
|
# outrank a generic new scan in current operator guidance.
|
|
_set_scanned_devices(service, [])
|
|
failed = service.state()
|
|
failed_operation = next(
|
|
item for item in failed["operations"] if item["action"] == "network.provision"
|
|
)
|
|
assert write_calls == 1
|
|
assert failed["selected_device_id"] is None
|
|
assert failed["device_session"] is None
|
|
assert failed["network_mutation_ledger"]["status"] == "unresolved"
|
|
assert failed["network_mutation_ledger"]["stage"] == "dispatching"
|
|
assert failed_operation["error"]["side_effect_status"] == "unknown"
|
|
assert failed_operation["error"]["safe_to_retry"] is False
|
|
assert failed_operation["context"]["automatic_retry"] is False
|
|
assert failed["connection_attempt"]["phase"] == "network_outcome_unknown"
|
|
assert failed["connection_attempt"]["side_effect_status"] == "unknown"
|
|
assert failed["connection_attempt"]["safe_next_action"] == "verify-control-read-only"
|
|
assert failed["connection_attempt"]["automatic_retry"] is False
|
|
assert (
|
|
failed["connection_policy"]["actions"]["observe-configured-device-network"]["allowed"]
|
|
is True
|
|
)
|
|
assert failed["connection_policy"]["recommended_action"] == (
|
|
"observe-configured-device-network"
|
|
)
|
|
assert failed["connection_policy"]["actions"]["scan-ble"]["allowed"] is True
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
assert (
|
|
service.state()["connection_policy"]["actions"]["provision-fresh-device"]["allowed"] is True
|
|
)
|
|
assert write_calls == 1
|
|
|
|
_SYNTHETIC_SCAN_CAPTURES.clear()
|
|
restarted, _ = service_with_fake_runtime(tmp_path)
|
|
terminal = restarted.state()
|
|
assert write_calls == 1
|
|
assert terminal["selected_device_id"] is None
|
|
assert terminal["device_session"] is None
|
|
assert terminal["network_write_reconciliation"] is None
|
|
assert terminal["network_mutation_ledger"]["status"] == "resolved"
|
|
assert terminal["network_mutation_ledger"]["resolution"] == "interrupted"
|
|
assert terminal["network_provisioning_idempotency"]["active_operation_id"] is None
|
|
durable_terminal = (
|
|
restarted._require_network_provisioning_idempotency_journal() # noqa: SLF001
|
|
.snapshot()
|
|
.records[-1]
|
|
)
|
|
assert durable_terminal.terminal is not None
|
|
assert durable_terminal.terminal.outcome_code == "network.provision.interrupted"
|
|
assert durable_terminal.terminal.side_effect_status == "reconciled"
|
|
assert durable_terminal.terminal.safe_to_retry is False
|
|
_set_scanned_k1(restarted, device_id="k1-a")
|
|
assert (
|
|
restarted.state()["connection_policy"]["actions"]["provision-fresh-device"]["allowed"]
|
|
is True
|
|
)
|
|
assert write_calls == 1
|
|
|
|
|
|
def test_power_loss_during_observation_preserves_audit_and_releases_ownership(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""CONN-13: bounded observation survives; selection and ownership do not."""
|
|
|
|
service, _ = service_with_fake_runtime(tmp_path)
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
baseline = _wifi_status_read(None, device_id="k1-a")["status"]
|
|
bounded_baseline = {key: baseline[key] for key in ("mode", "ipv4", "status_code", "reserved")}
|
|
write_calls = 0
|
|
|
|
async def power_lost_while_observing(
|
|
*_: object,
|
|
on_write_dispatch: Callable[[dict[str, Any], str], None] | None = None,
|
|
**__: object,
|
|
) -> dict[str, Any]:
|
|
nonlocal write_calls
|
|
write_calls += 1
|
|
assert on_write_dispatch is not None
|
|
on_write_dispatch(baseline, "with_response")
|
|
return {
|
|
"started_at_utc": "2026-08-08T10:00:00Z",
|
|
"completed_at_utc": "2026-08-08T10:00:01Z",
|
|
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
|
"outcome": "connection-lost-during-status-observation",
|
|
"write_mode": "with_response",
|
|
"baseline_status": baseline,
|
|
"observations": [{"status": baseline}],
|
|
}
|
|
|
|
monkeypatch.setattr(facade_module, "provision_wifi_once", power_lost_while_observing)
|
|
|
|
with pytest.raises(RuntimeError, match="не сообщило адрес"):
|
|
asyncio.run(
|
|
service.connect(
|
|
_connect_request(
|
|
device_id="k1-a",
|
|
ssid="lab-router",
|
|
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
|
compatibility_attestation=ATTESTATION,
|
|
)
|
|
)
|
|
)
|
|
|
|
failed = service.state()
|
|
failed_record = service._network_mutation_ledger.snapshot().record # noqa: SLF001
|
|
failed_operation = next(
|
|
item for item in failed["operations"] if item["action"] == "network.provision"
|
|
)
|
|
assert write_calls == 1
|
|
assert failed["selected_device_id"] is None
|
|
assert failed["device_session"] is None
|
|
assert failed["network_mutation_ledger"]["status"] == "unresolved"
|
|
assert failed["network_mutation_ledger"]["stage"] == "observing"
|
|
assert failed_record is not None
|
|
assert failed_record.write_confirmed is True
|
|
assert failed_record.last_observation is not None
|
|
assert failed_record.last_observation.as_dict() == bounded_baseline
|
|
assert failed_operation["error"]["side_effect_status"] == "unknown"
|
|
assert failed_operation["error"]["safe_to_retry"] is False
|
|
assert failed_operation["context"]["automatic_retry"] is False
|
|
assert failed["connection_policy"]["actions"]["scan-ble"]["allowed"] is True
|
|
_set_scanned_k1(service, device_id="k1-a")
|
|
assert (
|
|
service.state()["connection_policy"]["actions"]["provision-fresh-device"]["allowed"] is True
|
|
)
|
|
assert write_calls == 1
|
|
|
|
_SYNTHETIC_SCAN_CAPTURES.clear()
|
|
restarted, _ = service_with_fake_runtime(tmp_path)
|
|
terminal = restarted.state()
|
|
terminal_record = restarted._network_mutation_ledger.snapshot().record # noqa: SLF001
|
|
assert write_calls == 1
|
|
assert terminal["selected_device_id"] is None
|
|
assert terminal["device_session"] is None
|
|
assert terminal["network_write_reconciliation"] is None
|
|
assert terminal["network_mutation_ledger"]["status"] == "resolved"
|
|
assert terminal["network_mutation_ledger"]["resolution"] == "interrupted"
|
|
assert terminal_record is not None
|
|
assert terminal_record.last_observation is not None
|
|
assert terminal_record.last_observation.as_dict() == bounded_baseline
|
|
assert terminal["network_provisioning_idempotency"]["active_operation_id"] is None
|
|
_set_scanned_k1(restarted, device_id="k1-a")
|
|
assert (
|
|
restarted.state()["connection_policy"]["actions"]["provision-fresh-device"]["allowed"]
|
|
is True
|
|
)
|
|
assert write_calls == 1
|