fix: make offline teleop start one-shot and rearmable

This commit is contained in:
LengedZhao
2026-08-08 16:30:50 +08:00
parent 1a79fca36f
commit 29030c0b25
5 changed files with 1154 additions and 102 deletions

View File

@@ -7,8 +7,10 @@ import importlib.util
import json
from pathlib import Path
import sys
from threading import Event
from types import ModuleType
import unittest
from unittest import mock
fake_omnisocket = ModuleType("omnisocket")
@@ -53,6 +55,71 @@ def frame(pressed: bool) -> dict[str, object]:
class SessionGateTest(unittest.TestCase):
class FakeSession:
def __init__(
self,
responses: list[tuple[str, int, bytes]] | None = None,
) -> None:
self.responses = list(responses or [])
self.sent: list[tuple[str, bytes]] = []
self.closed = False
@staticmethod
def stats() -> dict[str, int]:
return {"connected": 1, "registered": 1}
@staticmethod
def kcp_stats() -> dict[str, int]:
return {
"snd_queue": 0,
"snd_buffer": 0,
"last_feedback_age_ms": 0,
}
def send(self, *, to: str, data: bytes) -> None:
self.sent.append((to, data))
def recv(self, timeout_ms: int) -> tuple[str, int, bytes] | None:
self.assert_zero_timeout(timeout_ms)
if self.responses:
return self.responses.pop(0)
return None
@staticmethod
def assert_zero_timeout(timeout_ms: int) -> None:
if timeout_ms != 0:
raise AssertionError(f"unexpected receive timeout: {timeout_ms}")
def close(self) -> None:
self.closed = True
class BlockingConnectSession(FakeSession):
def __init__(
self,
entered: Event,
release: Event,
failure: OSError | None = None,
) -> None:
super().__init__()
self.entered = entered
self.release = release
self.failure = failure
self.connect_args: tuple[str, str, dict[str, object]] | None = None
def connect(
self,
*,
server_addr: str,
peer_id: str,
**options: object,
) -> None:
self.connect_args = (server_addr, peer_id, options)
self.entered.set()
if not self.release.wait(timeout=1.0):
raise RuntimeError("test did not release blocked connect")
if self.failure is not None:
raise self.failure
def setUp(self) -> None:
self.sender = sender_module.XteleSender(args())
@@ -67,7 +134,43 @@ class SessionGateTest(unittest.TestCase):
self.assertFalse(self.sender.require_combo_release)
return finished_at
def test_boot_requires_stable_release_before_start(self) -> None:
def create_pending_start(self, started_at: float = 0.0) -> float:
released_at = self.stable_release(started_at)
self.assertIsNone(
self.sender._update_teleop_gate(released_at + 0.01, frame(True))
)
pending_at = released_at + 3.02
self.assertEqual(
self.sender._update_teleop_gate(pending_at, frame(True)),
"start_pending",
)
self.assertTrue(self.sender.start_pending)
self.assertFalse(self.sender.teleop_active)
return pending_at
def cancel_pending_from_latest_frame(self) -> None:
if (
self.sender.start_pending
and self.sender.latest_combo_state is not True
):
reason = (
"released"
if self.sender.latest_combo_state is False
else "malformed"
)
self.sender._cancel_pending_start(reason)
def attach_session(
self,
responses: list[tuple[str, int, bytes]] | None = None,
) -> FakeSession:
session = self.FakeSession(responses)
self.sender.session = session
self.sender.session_connected_at = 100.0
self.sender.counters["connected"] = 1
return session
def test_boot_requires_stable_release_before_pending_start(self) -> None:
self.assertIsNone(self.sender._update_teleop_gate(0.0, frame(True)))
self.assertIsNone(self.sender._update_teleop_gate(4.0, frame(True)))
released_at = self.stable_release(5.0)
@@ -76,44 +179,574 @@ class SessionGateTest(unittest.TestCase):
)
self.assertEqual(
self.sender._update_teleop_gate(released_at + 3.02, frame(True)),
"start",
"start_pending",
)
self.assertTrue(self.sender.teleop_active)
self.assertTrue(self.sender.start_pending)
self.assertFalse(self.sender.teleop_active)
self.assertEqual(self.sender.counters["teleop_start_requests"], 1)
self.assertEqual(self.sender.counters["teleop_starts"], 0)
self.assertIsNotNone(self.sender.teleop_session_id)
self.assertTrue(
self.sender._pending_connect_ready(released_at + 3.02)
)
def test_boot_malformed_buttons_cannot_clear_release_requirement(self) -> None:
malformed = frame(False)
malformed["button"] = {
"left": [0, 0, 0],
"right": [0, 0, "0"],
}
self.assertIsNone(self.sender._update_teleop_gate(0.0, frame(False)))
self.assertEqual(self.sender.combo_release_started_at, 0.0)
self.assertIsNone(self.sender._update_teleop_gate(0.49, malformed))
self.assertTrue(self.sender.require_combo_release)
self.assertIsNone(self.sender.combo_release_started_at)
self.assertIsNone(self.sender.latest_combo_state)
# The release timer must restart after malformed input; time before the
# malformed frame cannot be accumulated toward re-arming.
self.assertIsNone(self.sender._update_teleop_gate(0.50, frame(False)))
self.assertIsNone(self.sender._update_teleop_gate(0.99, frame(False)))
self.assertTrue(self.sender.require_combo_release)
self.assertIsNone(self.sender._update_teleop_gate(1.01, frame(False)))
self.assertFalse(self.sender.require_combo_release)
def test_start_stop_parser_accepts_only_explicit_boolean_buttons(self) -> None:
self.assertTrue(self.sender._start_stop_pressed(frame(True)))
self.assertFalse(self.sender._start_stop_pressed(frame(False)))
for invalid in (1.0, "1", None, 2, -1):
with self.subTest(value=invalid):
malformed = frame(True)
malformed["button"] = {
"left": [0, 0, invalid],
"right": [0, 0, 1],
}
self.assertIsNone(
self.sender._start_stop_pressed(malformed)
)
def test_single_false_frame_cannot_rearm_stop(self) -> None:
released_at = self.stable_release(0.0)
self.sender._update_teleop_gate(released_at + 0.01, frame(True))
self.assertEqual(
self.sender._update_teleop_gate(released_at + 3.02, frame(True)),
"start",
)
pending_at = self.create_pending_start()
self.sender._commit_pending_start_sent(pending_at + 0.01)
self.assertIsNone(
self.sender._update_teleop_gate(released_at + 3.03, frame(False))
self.sender._update_teleop_gate(pending_at + 0.02, frame(False))
)
self.assertIsNone(
self.sender._update_teleop_gate(released_at + 3.04, frame(True))
self.sender._update_teleop_gate(pending_at + 0.03, frame(True))
)
self.assertIsNone(
self.sender._update_teleop_gate(released_at + 7.00, frame(True))
self.sender._update_teleop_gate(pending_at + 4.00, frame(True))
)
self.assertTrue(self.sender.teleop_active)
self.assertTrue(self.sender.require_combo_release)
def test_stable_release_allows_separate_stop_hold(self) -> None:
released_at = self.stable_release(0.0)
self.sender._update_teleop_gate(released_at + 0.01, frame(True))
self.assertEqual(
self.sender._update_teleop_gate(released_at + 3.02, frame(True)),
"start",
)
second_release = self.stable_release(released_at + 3.03)
pending_at = self.create_pending_start()
self.sender._commit_pending_start_sent(pending_at + 0.01)
second_release = self.stable_release(pending_at + 0.02)
self.sender._update_teleop_gate(second_release + 0.01, frame(True))
self.assertEqual(
self.sender._update_teleop_gate(second_release + 3.02, frame(True)),
"stop",
)
self.assertFalse(self.sender.teleop_active)
self.assertEqual(self.sender.counters["teleop_starts"], 1)
self.assertEqual(self.sender.counters["teleop_stops"], 1)
def test_cancelled_start_requires_release_before_a_new_hold(self) -> None:
pending_at = self.create_pending_start()
first_session_id = self.sender.teleop_session_id
self.sender._cancel_pending_start("Hub offline")
self.assertFalse(self.sender.start_pending)
self.assertFalse(self.sender.teleop_active)
self.assertTrue(self.sender.require_combo_release)
self.assertEqual(self.sender.last_start_failure, "Hub offline")
self.assertIsNone(self.sender.teleop_session_id)
# A consumed hold cannot silently become another pending request while
# the operator keeps Z+C pressed, regardless of elapsed time.
self.assertIsNone(
self.sender._update_teleop_gate(
pending_at + 10.0, frame(True)
)
)
self.assertFalse(self.sender.start_pending)
self.assertFalse(self.sender._pending_connect_ready(pending_at + 10.0))
# A stable release followed by a new complete hold creates a distinct
# logical session which can make one new connection attempt.
released_at = self.stable_release(pending_at + 10.01)
self.assertIsNone(
self.sender._update_teleop_gate(
released_at + 0.01, frame(True)
)
)
self.assertEqual(
self.sender._update_teleop_gate(
released_at + 3.02, frame(True)
),
"start_pending",
)
self.assertTrue(self.sender.start_pending)
self.assertIsNotNone(self.sender.teleop_session_id)
self.assertNotEqual(self.sender.teleop_session_id, first_session_id)
self.assertEqual(self.sender.counters["teleop_start_requests"], 2)
self.assertEqual(self.sender.counters["teleop_starts"], 0)
def test_pending_start_release_cancels_and_closes_session(self) -> None:
pending_at = self.create_pending_start()
session = self.attach_session()
self.sender.start_wait_fresh_after_connect = True
self.assertIsNone(
self.sender._update_teleop_gate(pending_at + 0.01, frame(False))
)
self.cancel_pending_from_latest_frame()
self.assertFalse(self.sender.start_pending)
self.assertFalse(self.sender.teleop_active)
self.assertIsNone(self.sender.teleop_session_id)
self.assertIsNone(self.sender.session)
self.assertTrue(session.closed)
self.assertEqual(session.sent, [])
self.assertEqual(self.sender.counters["start_pending_cancels"], 1)
self.assertTrue(self.sender.require_combo_release)
self.assertFalse(self.sender._pending_connect_ready(pending_at + 10.0))
def test_pending_start_malformed_button_state_cancels(self) -> None:
pending_at = self.create_pending_start()
malformed = frame(True)
malformed["button"] = {"left": [0, 0], "right": [0, 0, 1]}
self.assertIsNone(
self.sender._update_teleop_gate(pending_at + 0.01, malformed)
)
self.cancel_pending_from_latest_frame()
self.assertFalse(self.sender.start_pending)
self.assertIsNone(self.sender.latest_combo_state)
self.assertEqual(self.sender.last_start_failure, "malformed")
self.assertEqual(self.sender.counters["start_pending_cancels"], 1)
def test_pending_freshness_waits_then_accepts_advancing_timestamp(self) -> None:
self.create_pending_start()
self.sender.start_wait_fresh_after_connect = True
first = frame(True)
first["timestamp"] = 100.0
second = frame(True)
second["timestamp"] = 100.01
self.assertEqual(
self.sender._check_pending_start_freshness(first),
("wait", ""),
)
self.assertEqual(self.sender.start_fresh_barrier_timestamp, 100.0)
self.assertTrue(self.sender.start_wait_fresh_after_connect)
self.assertFalse(self.sender.teleop_active)
self.assertEqual(
self.sender._check_pending_start_freshness(second),
("ready", ""),
)
self.assertIsNone(self.sender.start_fresh_barrier_timestamp)
self.assertFalse(self.sender.start_wait_fresh_after_connect)
self.assertTrue(self.sender.start_pending)
self.assertEqual(self.sender.counters["teleop_starts"], 0)
def test_pending_freshness_missing_timestamp_cancels(self) -> None:
self.create_pending_start()
session = self.attach_session()
self.sender.start_wait_fresh_after_connect = True
outcome, reason = self.sender._check_pending_start_freshness(
frame(True)
)
self.assertEqual(outcome, "cancel")
self.assertIn("fresh xTELE timestamp", reason)
self.sender._cancel_pending_start(reason)
self.assertFalse(self.sender.start_pending)
self.assertFalse(self.sender.teleop_active)
self.assertTrue(session.closed)
self.assertEqual(session.sent, [])
def test_pending_freshness_repeated_timestamp_waits_for_later_advance(
self,
) -> None:
self.create_pending_start()
self.sender.start_wait_fresh_after_connect = True
first = frame(True)
first["timestamp"] = 100.0
repeated = frame(True)
repeated["timestamp"] = 100.0
advanced = frame(True)
advanced["timestamp"] = 100.01
self.assertEqual(
self.sender._check_pending_start_freshness(first),
("wait", ""),
)
self.assertEqual(
self.sender._check_pending_start_freshness(repeated),
("wait", ""),
)
self.assertEqual(self.sender.start_fresh_barrier_timestamp, 100.0)
self.assertTrue(self.sender.start_wait_fresh_after_connect)
self.assertTrue(self.sender.start_pending)
self.assertEqual(
self.sender._check_pending_start_freshness(advanced),
("ready", ""),
)
self.assertIsNone(self.sender.start_fresh_barrier_timestamp)
self.assertFalse(self.sender.start_wait_fresh_after_connect)
self.assertTrue(self.sender.start_pending)
def test_pending_freshness_backwards_timestamp_cancels(self) -> None:
self.create_pending_start()
session = self.attach_session()
self.sender.start_wait_fresh_after_connect = True
first = frame(True)
first["timestamp"] = 100.0
backwards = frame(True)
backwards["timestamp"] = 99.99
self.assertEqual(
self.sender._check_pending_start_freshness(first),
("wait", ""),
)
outcome, reason = self.sender._check_pending_start_freshness(backwards)
self.assertEqual(outcome, "cancel")
self.assertIn("moved backwards", reason)
self.sender._cancel_pending_start(reason)
self.assertFalse(self.sender.start_pending)
self.assertTrue(session.closed)
self.assertEqual(session.sent, [])
def test_background_connect_success_is_adopted_as_pending(self) -> None:
self.create_pending_start()
entered = Event()
release = Event()
session = self.BlockingConnectSession(entered, release)
with (
mock.patch.object(sender_module, "Session", return_value=session),
mock.patch.object(self.sender, "write_status"),
):
self.assertTrue(self.sender._start_connect_attempt())
self.assertTrue(entered.wait(timeout=1.0))
self.assertTrue(self.sender._connect_inflight())
thread = self.sender.connect_thread
self.assertIsNotNone(thread)
release.set()
thread.join(timeout=1.0) # type: ignore[union-attr]
self.assertFalse(thread.is_alive()) # type: ignore[union-attr]
self.assertEqual(
self.sender._poll_connect_result(50.0), "connected"
)
self.assertIs(self.sender.session, session)
self.assertFalse(session.closed)
self.assertTrue(self.sender.start_pending)
self.assertFalse(self.sender.teleop_active)
self.assertTrue(self.sender.start_wait_fresh_after_connect)
self.assertIsNone(self.sender.start_fresh_barrier_timestamp)
self.assertEqual(self.sender.counters["connected"], 1)
self.assertEqual(self.sender.counters["reconnects"], 1)
self.assertEqual(
session.connect_args,
(self.sender.args.server, self.sender.args.peer_id, {}),
)
self.sender.close_session()
def test_release_during_connect_discards_and_closes_late_result(self) -> None:
pending_at = self.create_pending_start()
entered = Event()
release = Event()
session = self.BlockingConnectSession(entered, release)
with mock.patch.object(sender_module, "Session", return_value=session):
self.assertTrue(self.sender._start_connect_attempt())
self.assertTrue(entered.wait(timeout=1.0))
thread = self.sender.connect_thread
self.assertIsNotNone(thread)
self.assertIsNone(
self.sender._update_teleop_gate(
pending_at + 0.01, frame(False)
)
)
self.cancel_pending_from_latest_frame()
self.assertFalse(self.sender.start_pending)
release.set()
thread.join(timeout=1.0) # type: ignore[union-attr]
self.assertFalse(thread.is_alive()) # type: ignore[union-attr]
self.assertEqual(
self.sender._poll_connect_result(pending_at + 0.02),
"discarded",
)
self.assertTrue(session.closed)
self.assertIsNone(self.sender.session)
self.assertFalse(self.sender.teleop_active)
self.assertIsNone(self.sender.teleop_session_id)
self.assertEqual(self.sender.counters["connected"], 0)
self.assertEqual(self.sender.counters["teleop_starts"], 0)
self.assertEqual(self.sender.counters["start_pending_cancels"], 1)
def test_background_connect_failure_consumes_hold_without_retry(self) -> None:
pending_at = self.create_pending_start()
first_session_id = self.sender.teleop_session_id
entered = Event()
release = Event()
session = self.BlockingConnectSession(
entered,
release,
OSError("connection refused"),
)
with (
mock.patch.object(sender_module, "Session", return_value=session),
mock.patch.object(self.sender, "write_status"),
):
# run() owns this counter; mirror its one increment around the
# lower-level worker call used by this unit test.
self.sender.counters["start_connect_attempts"] += 1
self.assertTrue(self.sender._start_connect_attempt())
self.assertTrue(entered.wait(timeout=1.0))
thread = self.sender.connect_thread
self.assertIsNotNone(thread)
release.set()
thread.join(timeout=1.0) # type: ignore[union-attr]
self.assertFalse(thread.is_alive()) # type: ignore[union-attr]
self.assertEqual(self.sender._poll_connect_result(20.0), "failed")
self.assertTrue(session.closed)
self.assertEqual(session.sent, [])
self.assertIsNone(self.sender.session)
self.assertFalse(self.sender.start_pending)
self.assertFalse(self.sender.teleop_active)
self.assertIsNone(self.sender.teleop_session_id)
self.assertTrue(self.sender.require_combo_release)
self.assertEqual(self.sender.counters["start_connect_failures"], 1)
self.assertEqual(self.sender.counters["start_connect_attempts"], 1)
self.assertEqual(self.sender.counters["teleop_starts"], 0)
self.assertEqual(self.sender.counters["frames_sent"], 0)
self.assertIn("connection refused", self.sender.last_start_failure)
self.assertFalse(self.sender._pending_connect_ready(100.0))
# Keeping the failed Z+C hold pressed cannot create another request or
# connection attempt after the Hub later becomes available.
self.assertIsNone(
self.sender._update_teleop_gate(25.0, frame(True))
)
self.assertFalse(self.sender.start_pending)
self.assertEqual(self.sender.counters["teleop_start_requests"], 1)
self.assertEqual(self.sender.counters["start_connect_attempts"], 1)
released_at = self.stable_release(25.01)
self.assertIsNone(
self.sender._update_teleop_gate(
released_at + 0.01, frame(True)
)
)
self.assertEqual(
self.sender._update_teleop_gate(
released_at + 3.02, frame(True)
),
"start_pending",
)
self.assertTrue(self.sender.start_pending)
self.assertNotEqual(self.sender.teleop_session_id, first_session_id)
self.assertEqual(self.sender.counters["teleop_start_requests"], 2)
self.assertTrue(self.sender._pending_connect_ready(released_at + 3.02))
def test_session_constructor_failure_consumes_hold_without_retry(self) -> None:
self.create_pending_start()
constructor_called = Event()
def broken_session_constructor() -> object:
constructor_called.set()
raise RuntimeError("Session constructor failed")
with (
mock.patch.object(
sender_module,
"Session",
side_effect=broken_session_constructor,
),
mock.patch.object(self.sender, "write_status"),
):
self.assertTrue(self.sender._start_connect_attempt())
self.assertTrue(constructor_called.wait(timeout=1.0))
thread = self.sender.connect_thread
self.assertIsNotNone(thread)
thread.join(timeout=1.0) # type: ignore[union-attr]
self.assertFalse(thread.is_alive()) # type: ignore[union-attr]
self.assertEqual(self.sender._poll_connect_result(30.0), "failed")
self.assertIsNone(self.sender.session)
self.assertFalse(self.sender.start_pending)
self.assertFalse(self.sender.teleop_active)
self.assertIsNone(self.sender.teleop_session_id)
self.assertTrue(self.sender.require_combo_release)
self.assertEqual(self.sender.counters["start_connect_failures"], 1)
self.assertIn("Session constructor failed", self.sender.last_start_failure)
self.assertEqual(self.sender.counters["teleop_starts"], 0)
self.assertFalse(self.sender._pending_connect_ready(300.0))
def test_successful_start_commit_activates_once(self) -> None:
pending_at = self.create_pending_start()
self.sender._commit_pending_start_sent(pending_at + 0.01)
self.assertFalse(self.sender.start_pending)
self.assertTrue(self.sender.teleop_active)
self.assertEqual(self.sender.counters["teleop_starts"], 1)
self.assertEqual(self.sender.counters["teleop_start_requests"], 1)
def test_unknown_target_is_an_ordinary_remote_error_and_aborts(self) -> None:
pending_at = self.create_pending_start()
self.sender._commit_pending_start_sent(pending_at + 0.01)
session = self.attach_session(
[("hub", fake_omnisocket.MSG_TYPE_ERROR, b"unknown target: robot")]
)
self.sender._drain_responses()
self.assertTrue(session.closed)
self.assertIsNone(self.sender.session)
self.assertFalse(self.sender.start_pending)
self.assertFalse(self.sender.teleop_active)
self.assertIsNone(self.sender.teleop_session_id)
self.assertTrue(self.sender.require_combo_release)
self.assertEqual(self.sender.counters["teleop_aborts"], 1)
self.assertEqual(self.sender.counters["teleop_starts"], 1)
self.assertIn("unknown target: robot", self.sender.last_error)
def test_other_remote_error_also_aborts(self) -> None:
pending_at = self.create_pending_start()
self.sender._commit_pending_start_sent(pending_at + 0.01)
session = self.attach_session(
[("hub", fake_omnisocket.MSG_TYPE_ERROR, b"route unavailable")]
)
self.sender._drain_responses()
self.assertTrue(session.closed)
self.assertFalse(self.sender.start_pending)
self.assertFalse(self.sender.teleop_active)
self.assertEqual(self.sender.counters["teleop_aborts"], 1)
self.assertIn("route unavailable", self.sender.last_error)
def test_source_clock_allows_adjacent_repeat_under_timeout(self) -> None:
first = frame(False)
first["timestamp"] = 2507
repeated = frame(False)
repeated["timestamp"] = 2507
self.assertEqual(
self.sender._check_source_clock(10.0, first),
(True, ""),
)
self.assertEqual(
self.sender._check_source_clock(10.24, repeated),
(True, ""),
)
self.assertEqual(self.sender.last_source_timestamp, 2507.0)
self.assertEqual(self.sender.last_source_timestamp_advanced_at, 10.0)
def test_source_clock_rejects_persistent_freeze_over_timeout(self) -> None:
first = frame(False)
first["timestamp"] = 2517
repeated = frame(False)
repeated["timestamp"] = 2517
self.assertEqual(
self.sender._check_source_clock(10.0, first),
(True, ""),
)
self.assertEqual(
self.sender._check_source_clock(10.24, repeated),
(True, ""),
)
clock_ok, reason = self.sender._check_source_clock(10.251, repeated)
self.assertFalse(clock_ok)
self.assertIn("stopped advancing", reason)
self.assertEqual(self.sender.last_source_timestamp, 2517.0)
def test_source_clock_rejects_backwards_timestamp_while_active(self) -> None:
first = frame(False)
first["timestamp"] = 3000
backwards = frame(False)
backwards["timestamp"] = 2999
self.assertEqual(
self.sender._check_source_clock(10.0, first),
(True, ""),
)
self.sender.teleop_active = True
clock_ok, reason = self.sender._check_source_clock(10.01, backwards)
self.assertFalse(clock_ok)
self.assertIn("moved backwards", reason)
self.assertEqual(self.sender.last_source_timestamp, 3000.0)
self.assertEqual(self.sender.counters["source_timestamp_resets"], 0)
def test_source_clock_idle_backwards_resets_and_requires_release(self) -> None:
first = frame(False)
first["timestamp"] = 4000
restarted = frame(True)
restarted["timestamp"] = 10
self.assertEqual(
self.sender._check_source_clock(10.0, first),
(True, ""),
)
self.sender.require_combo_release = False
self.sender.combo_started_at = 10.01
self.sender.combo_release_started_at = 10.02
self.sender.latest_combo_state = True
self.assertEqual(
self.sender._check_source_clock(10.03, restarted),
(True, ""),
)
self.assertEqual(self.sender.last_source_timestamp, 10.0)
self.assertEqual(self.sender.last_source_timestamp_advanced_at, 10.03)
self.assertTrue(self.sender.require_combo_release)
self.assertIsNone(self.sender.combo_started_at)
self.assertIsNone(self.sender.combo_release_started_at)
self.assertIsNone(self.sender.latest_combo_state)
self.assertEqual(self.sender.counters["source_timestamp_resets"], 1)
def test_source_clock_rejects_missing_timestamp(self) -> None:
clock_ok, reason = self.sender._check_source_clock(10.0, frame(False))
self.assertFalse(clock_ok)
self.assertIn("missing or malformed", reason)
self.assertIsNone(self.sender.last_source_timestamp)
def test_finish_stop_session_clears_state_and_closes_session(self) -> None:
session = self.attach_session()
self.sender.teleop_active = True
self.sender.teleop_session_id = "active-session"
self.sender.teleop_session_seq = 42
self.sender.start_markers_remaining = 7
self.sender._finish_stop_session()
self.assertFalse(self.sender.teleop_active)
self.assertIsNone(self.sender.teleop_session_id)
self.assertEqual(self.sender.teleop_session_seq, 0)
self.assertEqual(self.sender.start_markers_remaining, 0)
self.assertIsNone(self.sender.session)
self.assertTrue(session.closed)
self.assertEqual(session.sent, [])
self.assertEqual(self.sender.counters["connected"], 0)
def test_untrusted_transport_metadata_is_overwritten(self) -> None:
data = frame(False)