NODEDC_MISSION_CORE/tests/test_xgrids_application_ses...

219 lines
6.6 KiB
Python

from __future__ import annotations
import threading
import time
from dataclasses import dataclass
from typing import Any
import pytest
from k1link.device_plugins.xgrids_k1.protocol import application_session as session_module
from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
ApplicationControlAuthority,
LiveDeviceControlBinding,
)
from k1link.device_plugins.xgrids_k1.protocol.application_session import (
InteractiveApplicationControlSession,
OperatorPresenceConfirmation,
)
APPLICATION_KEY = "00000000-0000-0000-0000-000000000000"
class FakeAuthorityLoader:
def __init__(self) -> None:
self.calls = 0
def load(self) -> ApplicationControlAuthority:
self.calls += 1
return ApplicationControlAuthority(openapi_key=APPLICATION_KEY)
@dataclass
class FakeTransportSnapshot:
state: str
ready: bool
def as_dict(self) -> dict[str, object]:
return {
"state": self.state,
"latest_device_session_state": "ready" if self.ready else "scanning",
"latest_device_project_bound": not self.ready,
"automatic_retry": False,
"automatic_reconnect": False,
}
class FakeTransport:
def __init__(self, host: str) -> None:
self.host = host
self.state = "new"
self.ready = True
def open(self) -> FakeTransportSnapshot:
self.state = "ready"
return self.snapshot()
def close(self) -> None:
self.state = "closed"
def snapshot(self) -> FakeTransportSnapshot:
return FakeTransportSnapshot(self.state, self.ready)
class FakeExecutor:
records: list[str] = []
def __init__(self, transport: FakeTransport) -> None:
self.transport = transport
self.binding = LiveDeviceControlBinding(
vendor_device_id="device-id",
device_serial="serial-id",
software_version="3.0.2",
system_version="3.0.2",
device_model="K1",
device_type="scanner",
is_activated=True,
)
def run_connection_stage(self, _orchestrator: object) -> LiveDeviceControlBinding:
self.records.append("connection:1-6")
return self.binding
def wait_for_operator_checkpoint(
self,
event: str,
observed: Any,
) -> str:
self.records.append(f"wait:{event}")
deadline = time.monotonic() + 2.0
while not observed():
if time.monotonic() >= deadline:
raise TimeoutError(f"test did not release {event}")
threading.Event().wait(0.005)
return event
def run_workspace_entry_stage(
self,
_orchestrator: object,
_checkpoint: object,
) -> LiveDeviceControlBinding:
self.records.append("workspace:7")
return self.binding
def run_project_prompt_stage(
self,
_orchestrator: object,
_checkpoint: object,
) -> LiveDeviceControlBinding:
self.records.append("project:8-10")
return self.binding
def execute_canonical_start(self, *_args: object, **_kwargs: object) -> object:
self.records.append("start:11-14")
self.transport.ready = False
return object()
def maintain_active_until_stop_requested(self, observed: Any) -> None:
self.records.append("wait:stop")
while not observed():
threading.Event().wait(0.005)
def execute_canonical_stop(self, *_args: object, **_kwargs: object) -> object:
self.records.append("stop")
self.transport.ready = True
return object()
def maintain_post_stop_until_standby_confirmed(self, observed: Any) -> None:
self.records.append("wait:steady-green")
while not observed():
threading.Event().wait(0.005)
def snapshot(self) -> dict[str, object]:
return {"records": tuple(self.records), "automatic_retry": False}
def _confirmation() -> OperatorPresenceConfirmation:
return OperatorPresenceConfirmation(
operator_present=True,
owner_controlled_device=True,
lixelgo_closed=True,
battery_storage_confirmed=True,
expected_physical_state_confirmed=True,
)
def _wait_phase(
session: InteractiveApplicationControlSession,
expected: str,
) -> dict[str, object]:
deadline = time.monotonic() + 2.0
while time.monotonic() < deadline:
snapshot = session.snapshot()
if snapshot["state"] == expected:
return snapshot
threading.Event().wait(0.005)
raise AssertionError(f"session did not reach {expected}: {session.snapshot()}")
def test_every_canonical_stage_requires_a_separate_operator_event(
monkeypatch: pytest.MonkeyPatch,
) -> None:
FakeExecutor.records = []
monkeypatch.setattr(
session_module,
"PhysicalAcceptanceDialogueExecutor",
FakeExecutor,
)
loader = FakeAuthorityLoader()
transport = FakeTransport("192.168.1.20")
session = InteractiveApplicationControlSession(
loader,
transport_factory=lambda _host: transport, # type: ignore[arg-type]
epoch_seconds=lambda: 1_752_680_000,
)
session.open(
host="192.168.1.20",
timezone_name="Europe/Moscow",
confirmation=_confirmation(),
)
_wait_phase(session, "connection-ready")
threading.Event().wait(0.02)
assert FakeExecutor.records == ["connection:1-6", "wait:workspace-entered"]
session.enter_workspace()
_wait_phase(session, "workspace-ready")
assert FakeExecutor.records[-1] == "wait:project-prompt-opened"
session.open_project_prompt()
_wait_phase(session, "project-ready")
assert FakeExecutor.records[-1] == "wait:start-confirmed"
session.request_start(project_name="TEST001", confirmation=_confirmation())
_wait_phase(session, "scanning")
assert FakeExecutor.records[-1] == "wait:stop"
session.request_stop(confirmation=_confirmation())
standby = _wait_phase(session, "awaiting-standby-confirmation")
assert standby["can_confirm_standby"] is True
assert FakeExecutor.records[-1] == "wait:steady-green"
session.confirm_standby()
completed = _wait_phase(session, "completed")
assert loader.calls == 1
assert completed["automatic_retry"] is False
assert completed["scripted_transitions"] is False
assert FakeExecutor.records == [
"connection:1-6",
"wait:workspace-entered",
"workspace:7",
"wait:project-prompt-opened",
"project:8-10",
"wait:start-confirmed",
"start:11-14",
"wait:stop",
"stop",
"wait:steady-green",
]