test(v1): lock reconnect and disconnect-no-alarm behavior; close T-203

Adds a mid-stream drop recovery test and a pipeline test proving a
disconnected frame yields no events and interrupts a pending suspect
(stream loss never alarms). Live preview and fall detection on the real
Hikvision stream were confirmed on Windows. 74 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ila
2026-07-21 23:49:21 +08:00
co-authored by Claude Opus 4.8
parent b35082a643
commit f1a8d0ea47
5 changed files with 70 additions and 3 deletions
+16
View File
@@ -121,6 +121,22 @@ def test_pipeline_from_config_uses_runtime_version_for_confirmed_event():
assert result.events[0].config_version == config.runtime_config_version
def test_disconnect_interrupts_suspect_without_alarm():
pipeline, machine = _pipeline([(_pose(),), (_pose(horizontal=True),)])
pipeline.process(_packet(0.0))
pipeline.process(_packet(0.1))
assert machine.state_of("P-0001") is FallState.SUSPECT
outage = pipeline.process(
FramePacket(image=None, timestamp_monotonic=1.0, status=SourceStatus.RETRYING)
)
assert outage.events == ()
assert outage.people == ()
assert machine.state_of("P-0001") is FallState.NORMAL
def test_from_config_gives_each_run_a_unique_event_id():
config = _config()
+42
View File
@@ -93,6 +93,48 @@ def test_reconnect_waits_then_reopens_with_bounded_backoff():
assert recovered.image is not None
class _FlakyCapture:
def __init__(self, read_results):
self._read_results = list(read_results)
def isOpened(self):
return True
def read(self):
ok = self._read_results.pop(0) if self._read_results else False
return (True, np.zeros((8, 8, 3), dtype=np.uint8)) if ok else (False, None)
def get(self, _property_id):
return 0.0
def release(self):
pass
def test_stream_recovers_after_mid_stream_drop():
captures = [_FlakyCapture([True]), _OpenCapture()]
source = VideoSource(
"rtsp://demo",
mode=SourceMode.STREAM,
retry_initial_seconds=2.0,
retry_max_seconds=2.0,
capture_factory=lambda _source: captures.pop(0),
)
connected = source.read(now=0.0)
dropped = source.read(now=1.0)
waiting = source.read(now=1.5)
recovered = source.read(now=3.0)
assert connected.status is SourceStatus.CONNECTED
assert dropped.status is SourceStatus.RETRYING
assert dropped.image is None
assert waiting.status is SourceStatus.RETRYING
assert recovered.status is SourceStatus.CONNECTED
assert recovered.image is not None
class _NegativeFirstTimestampCapture:
def __init__(self):
self._read_count = 0