Files
NODEDC_MISSION_CORE/tests/test_xgrids_camera_gateway.py
T

1894 lines
73 KiB
Python

from __future__ import annotations
import asyncio
import hashlib
import json
import sys
import threading
import time
from collections.abc import Callable
from io import BytesIO
from pathlib import Path
import pytest
import k1link.device_plugins.xgrids_k1.camera as camera_module
from k1link.device_plugins.xgrids_k1.camera import (
CAMERA_MEDIA_TYPE,
CAMERA_PREVIEW_SEND_TIMEOUT_SECONDS,
CAMERA_SOURCE_IO_TIMEOUT_MICROSECONDS,
MAX_CAMERA_PREVIEW_CONSUMERS,
MAX_CAMERA_PREVIEW_QUEUE_AGE_SECONDS,
MAX_CAMERA_PREVIEW_QUEUED_BYTES,
MAX_CAMERA_PREVIEW_QUEUED_SEGMENTS,
MAX_FMP4_SEGMENT_BYTES,
CommittedCameraSegment,
XgridsK1CameraGateway,
_build_ffmpeg_argv,
_CameraPreviewSegmentQueue,
_read_mp4_box,
build_xgrids_k1_camera_router,
classify_camera_recording_health,
)
from k1link.device_plugins.xgrids_k1.connection_supervisor import (
EndpointTarget,
HostPathProbeResult,
VerifiedControlEvidence,
)
from k1link.device_plugins.xgrids_k1.facade import (
XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
XGRIDS_K1_PLUGIN_ID,
CameraPreviewSelectRequest,
CameraPreviewStopRequest,
XgridsK1CompatibilityService,
)
def _fake_ffmpeg(tmp_path: Path) -> Path:
executable = tmp_path / "fake-ffmpeg"
executable.write_text(
f"#!{sys.executable}\n"
"import sys, time\n"
"def box(kind, payload=b''):\n"
" return (8 + len(payload)).to_bytes(4, 'big') + kind + payload\n"
"payload = (box(b'ftyp', b'isom') + box(b'moov') + "
"box(b'moof') + box(b'mdat', b'frame'))\n"
"sys.stdout.buffer.write(payload)\n"
"sys.stdout.buffer.flush()\n"
"time.sleep(10)\n",
encoding="utf-8",
)
executable.chmod(0o700)
return executable
def _burst_ffmpeg(tmp_path: Path) -> Path:
executable = tmp_path / "burst-ffmpeg"
executable.write_text(
f"#!{sys.executable}\n"
"import sys, time\n"
"def box(kind, payload=b''):\n"
" return (8 + len(payload)).to_bytes(4, 'big') + kind + payload\n"
"sys.stdout.buffer.write(box(b'ftyp', b'isom') + box(b'moov'))\n"
"sys.stdout.buffer.flush()\n"
"time.sleep(0.2)\n"
"for index in range(10):\n"
" sys.stdout.buffer.write(box(b'moof') + box(b'mdat', bytes([index])))\n"
" sys.stdout.buffer.flush()\n"
" time.sleep(0.01)\n"
"time.sleep(10)\n",
encoding="utf-8",
)
executable.chmod(0o700)
return executable
def _preview_buffer_ffmpeg(
tmp_path: Path,
*,
fragments: int,
payload_bytes: int,
interval_seconds: float = 0.002,
) -> Path:
executable = tmp_path / f"preview-buffer-{fragments}-{payload_bytes}"
executable.write_text(
f"#!{sys.executable}\n"
"import sys, time\n"
"def box(kind, payload=b''):\n"
" return (8 + len(payload)).to_bytes(4, 'big') + kind + payload\n"
"sys.stdout.buffer.write(box(b'ftyp', b'isom') + box(b'moov'))\n"
"sys.stdout.buffer.flush()\n"
f"payload = bytes({payload_bytes})\n"
f"for index in range({fragments}):\n"
" sys.stdout.buffer.write(box(b'moof') + box(b'mdat', payload))\n"
" sys.stdout.buffer.flush()\n"
f" time.sleep({interval_seconds!r})\n"
"time.sleep(10)\n",
encoding="utf-8",
)
executable.chmod(0o700)
return executable
def _single_fragment_ffmpeg(tmp_path: Path, *, complete_fragment_bytes: int) -> Path:
assert complete_fragment_bytes >= 16
executable = tmp_path / f"single-fragment-{complete_fragment_bytes}"
executable.write_text(
f"#!{sys.executable}\n"
"import sys, time\n"
"def box(kind, payload=b''):\n"
" return (8 + len(payload)).to_bytes(4, 'big') + kind + payload\n"
"sys.stdout.buffer.write(box(b'ftyp', b'isom') + box(b'moov'))\n"
"sys.stdout.buffer.flush()\n"
f"payload_bytes = {complete_fragment_bytes} - 16\n"
"payload = b'PRIVATE-FRAME-BYTES' + bytes(payload_bytes - 19)\n"
"sys.stdout.buffer.write(box(b'moof') + box(b'mdat', payload))\n"
"sys.stdout.buffer.flush()\n"
"time.sleep(10)\n",
encoding="utf-8",
)
executable.chmod(0o700)
return executable
def _silent_ffmpeg(tmp_path: Path) -> Path:
executable = tmp_path / "silent-ffmpeg"
executable.write_text(
f"#!{sys.executable}\n"
"import time\n"
"time.sleep(10)\n",
encoding="utf-8",
)
executable.chmod(0o700)
return executable
def _clean_source_end_ffmpeg(tmp_path: Path) -> Path:
executable = tmp_path / "clean-source-end-ffmpeg"
executable.write_text(
f"#!{sys.executable}\n"
"import sys, time\n"
"def box(kind, payload=b''):\n"
" return (8 + len(payload)).to_bytes(4, 'big') + kind + payload\n"
"sys.stdout.buffer.write(box(b'ftyp', b'isom') + box(b'moov'))\n"
"sys.stdout.buffer.write(box(b'moof') + box(b'mdat', b'final-frame'))\n"
"sys.stdout.buffer.flush()\n"
"time.sleep(0.15)\n",
encoding="utf-8",
)
executable.chmod(0o700)
return executable
def _recoverable_source_end_ffmpeg(tmp_path: Path) -> tuple[Path, Path]:
executable = tmp_path / "recoverable-source-end-ffmpeg"
invocation_count = tmp_path / "recoverable-source-end-count"
executable.write_text(
f"#!{sys.executable}\n"
"import pathlib, sys, time\n"
"def box(kind, payload=b''):\n"
" return (8 + len(payload)).to_bytes(4, 'big') + kind + payload\n"
f"counter = pathlib.Path({str(invocation_count)!r})\n"
"count = int(counter.read_text()) + 1 if counter.exists() else 1\n"
"counter.write_text(str(count))\n"
"sys.stdout.buffer.write(box(b'ftyp', b'isom') + box(b'moov'))\n"
"sys.stdout.buffer.write(box(b'moof') + box(b'mdat', b'frame'))\n"
"sys.stdout.buffer.flush()\n"
"time.sleep(0.15 if count == 1 else 10)\n",
encoding="utf-8",
)
executable.chmod(0o700)
return executable, invocation_count
def _gated_media_progress_ffmpeg(
tmp_path: Path,
*,
release_second_media: Path,
) -> Path:
executable = tmp_path / "gated-media-progress-ffmpeg"
first_media_ready = tmp_path / "gated-media-progress-first-ready"
executable.write_text(
f"#!{sys.executable}\n"
"import pathlib, sys, time\n"
"def box(kind, payload=b''):\n"
" return (8 + len(payload)).to_bytes(4, 'big') + kind + payload\n"
"sys.stdout.buffer.write(box(b'ftyp', b'isom') + box(b'moov'))\n"
"sys.stdout.buffer.write(box(b'moof') + box(b'mdat', b'first'))\n"
"sys.stdout.buffer.flush()\n"
f"pathlib.Path({str(first_media_ready)!r}).write_text('ready')\n"
f"release = pathlib.Path({str(release_second_media)!r})\n"
"deadline = time.monotonic() + 5\n"
"while not release.exists() and time.monotonic() < deadline:\n"
" time.sleep(0.005)\n"
"if release.exists():\n"
" sys.stdout.buffer.write(box(b'moof') + box(b'mdat', b'second'))\n"
" sys.stdout.buffer.flush()\n"
"time.sleep(10)\n",
encoding="utf-8",
)
executable.chmod(0o700)
return executable
def _buffered_tail_ffmpeg(tmp_path: Path, sentinel: Path, *, fragments: int) -> Path:
executable = tmp_path / "buffered-tail-ffmpeg"
executable.write_text(
f"#!{sys.executable}\n"
"import pathlib, sys, time\n"
"def box(kind, payload=b''):\n"
" return (8 + len(payload)).to_bytes(4, 'big') + kind + payload\n"
"sys.stdout.buffer.write(box(b'ftyp', b'isom') + box(b'moov'))\n"
f"for index in range({fragments}):\n"
" sys.stdout.buffer.write(box(b'moof') + box(b'mdat', index.to_bytes(2, 'big')))\n"
"sys.stdout.buffer.flush()\n"
f"pathlib.Path({str(sentinel)!r}).write_text('ready')\n"
"time.sleep(10)\n",
encoding="utf-8",
)
executable.chmod(0o700)
return executable
def _incomplete_tail_ffmpeg(tmp_path: Path, sentinel: Path) -> Path:
executable = tmp_path / "incomplete-tail-ffmpeg"
executable.write_text(
f"#!{sys.executable}\n"
"import pathlib, sys, time\n"
"def box(kind, payload=b''):\n"
" return (8 + len(payload)).to_bytes(4, 'big') + kind + payload\n"
"sys.stdout.buffer.write(box(b'ftyp', b'isom') + box(b'moov') + box(b'moof'))\n"
"sys.stdout.buffer.flush()\n"
f"pathlib.Path({str(sentinel)!r}).write_text('ready')\n"
"time.sleep(10)\n",
encoding="utf-8",
)
executable.chmod(0o700)
return executable
def _gateway(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> XgridsK1CameraGateway:
monkeypatch.setenv("MISSIONCORE_FFMPEG_BINARY", str(_fake_ffmpeg(tmp_path)))
return XgridsK1CameraGateway(tmp_path, XGRIDS_K1_PLUGIN_ID)
def _wait_until(predicate: object, *, timeout: float = 3.0) -> None:
assert callable(predicate)
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if predicate():
return
time.sleep(0.01)
raise AssertionError("condition was not reached before timeout")
def test_camera_selection_is_exclusive_and_hides_device_transport(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
gateway = _gateway(tmp_path, monkeypatch)
try:
left = gateway.select("sensor.camera.left", "192.168.8.52")
assert left["active_source_id"] == "sensor.camera.left"
assert left["generation"] == 1
assert left["activation"]["max_active"] == 1
assert left["delivery"]["kind"] == "mse-fmp4-websocket"
assert left["delivery"]["media_type"] == CAMERA_MEDIA_TYPE
serialized = json.dumps(left)
assert "192.168.8.52" not in serialized
assert "rtsp://" not in serialized
assert "chn_left_main" not in serialized
right = gateway.select("sensor.camera.right", "192.168.8.52")
assert right["active_source_id"] == "sensor.camera.right"
assert right["generation"] == 2
assert right["delivery"]["url"].endswith("/camera-preview/2")
with pytest.raises(ValueError, match="generation"):
gateway.stop(1)
assert gateway.snapshot()["active_source_id"] == "sensor.camera.right"
stopped = gateway.stop(2)
assert stopped["phase"] == "idle"
assert stopped["delivery"] is None
finally:
gateway.close()
def test_camera_derived_observer_runs_only_after_durable_archive_commit(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("MISSIONCORE_FFMPEG_BINARY", str(_fake_ffmpeg(tmp_path)))
session = tmp_path / "session"
session.mkdir()
observed: list[CommittedCameraSegment] = []
def observe(segment: CommittedCameraSegment) -> None:
archive = session / "media" / segment.source_id / f"epoch-{segment.generation}"
committed = (
archive / "init.mp4"
if segment.kind == "init"
else archive / "segments" / f"{segment.sequence}.m4s"
)
assert committed.read_bytes() == segment.payload
observed.append(segment)
gateway = XgridsK1CameraGateway(
tmp_path,
XGRIDS_K1_PLUGIN_ID,
committed_segment_observer=observe,
)
try:
gateway.start_recording(session)
gateway.select("sensor.camera.right", "192.168.8.52")
_wait_until(lambda: len(observed) == 2)
assert [segment.kind for segment in observed] == ["init", "media"]
assert gateway.snapshot()["derived_observer_errors"] == 0
finally:
gateway.close()
def test_live_perception_rejects_late_camera_generation_after_replacement(
tmp_path: Path,
) -> None:
service = XgridsK1CompatibilityService(tmp_path)
try:
service.live_perception_ingress.begin_session("acquisition-new")
service._live_perception_camera_binding = ( # noqa: SLF001
"acquisition-new",
"sensor.camera.right",
12,
)
stale = CommittedCameraSegment(
source_id="sensor.camera.right",
generation=11,
kind="media",
sequence=4,
host_epoch_ns=10,
host_monotonic_ns=20,
payload=b"stale",
)
current = CommittedCameraSegment(
source_id="sensor.camera.right",
generation=12,
kind="media",
sequence=1,
host_epoch_ns=30,
host_monotonic_ns=40,
payload=b"current",
)
service._observe_committed_camera_segment(stale) # noqa: SLF001
after_stale = service.live_perception_ingress.snapshot()
assert after_stale["queues"]["camera-frame"]["published"] == 0
assert after_stale["queues"]["camera-frame"]["depth"] == 0
service._observe_committed_camera_segment(current) # noqa: SLF001
after_current = service.live_perception_ingress.snapshot()
assert after_current["queues"]["camera-frame"]["published"] == 1
assert after_current["queues"]["camera-frame"]["depth"] == 1
finally:
service.close()
def test_camera_ffmpeg_command_is_allowlisted_copy_remux() -> None:
argv = _build_ffmpeg_argv(
Path("/trusted/ffmpeg"),
"10.0.0.24",
"sensor.camera.left",
)
assert argv[0] == "/trusted/ffmpeg"
assert argv[argv.index("-i") + 1] == ("rtsp://10.0.0.24:8554/live/chn_left_main")
assert argv[argv.index("-c:v") + 1] == "copy"
assert argv[argv.index("-allowed_media_types") + 1] == "video"
assert argv[argv.index("-timeout") + 1] == str(CAMERA_SOURCE_IO_TIMEOUT_MICROSECONDS)
assert argv[argv.index("-flush_packets") + 1] == "1"
assert "-c:v" in argv
assert ";" not in " ".join(argv)
with pytest.raises(ValueError, match="private IPv4"):
_build_ffmpeg_argv(
Path("/trusted/ffmpeg"),
"example.com",
"sensor.camera.left",
)
def test_iso_bmff_reader_preserves_complete_boxes() -> None:
def box(kind: bytes, payload: bytes = b"") -> bytes:
return (8 + len(payload)).to_bytes(4, "big") + kind + payload
stream = BytesIO(box(b"ftyp", b"isom") + box(b"moov") + box(b"moof"))
assert _read_mp4_box(stream) == (b"ftyp", box(b"ftyp", b"isom"))
assert _read_mp4_box(stream) == (b"moov", box(b"moov"))
assert _read_mp4_box(stream) == (b"moof", box(b"moof"))
oversized = (9 * 1024 * 1024).to_bytes(4, "big") + b"mdat"
with pytest.raises(ValueError, match="unbounded"):
_read_mp4_box(BytesIO(oversized))
def test_current_epoch_is_not_media_ready_from_popen_or_epoch_allocation(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("MISSIONCORE_FFMPEG_BINARY", str(_silent_ffmpeg(tmp_path)))
gateway = XgridsK1CameraGateway(tmp_path, XGRIDS_K1_PLUGIN_ID)
session = tmp_path / "sessions" / "camera-pending-media"
session.mkdir(parents=True)
try:
selected = gateway.select("sensor.camera.right", "192.168.1.20")
first = gateway.start_recording(session)
assert first["phase"] == "connecting"
assert first["recording"]["active_epoch"] == selected["generation"]
assert first["recording"]["media_ready"] is False
assert first["recording"]["current_epoch"] == {
"generation": selected["generation"],
"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,
}
restarted = gateway.restart_recording_producer(
"sensor.camera.right",
"192.168.1.20",
expected_generation=selected["generation"],
expected_recording_session=session.name,
expected_active_epoch=selected["generation"],
expected_recording_media_segment_count=0,
pre_detach_fence=lambda reserve: reserve(),
)
replacement = selected["generation"] + 1
assert restarted["generation"] == replacement
assert restarted["recording"]["active_epoch"] == replacement
assert restarted["recording"]["media_ready"] is False
assert restarted["recording"]["current_epoch"]["generation"] == replacement
assert restarted["recording"]["current_epoch"]["init_committed"] is False
assert restarted["recording"]["current_epoch"]["first_media_committed"] is False
finally:
gateway.close()
def test_camera_health_classifier_splits_no_init_and_no_media_stalls() -> None:
base = {
"phase": "connecting",
"recording": {
"active": True,
"producer_age_ms": 10_000,
"current_epoch": {
"generation": 4,
"init_committed": False,
"init_committed_age_ms": None,
"first_media_committed": False,
},
},
"error": None,
}
assert classify_camera_recording_health(base) == "camera-connecting-no-init-stalled"
current = base["recording"]["current_epoch"]
current.update({"init_committed": True, "init_committed_age_ms": 10_000})
assert classify_camera_recording_health(base) == "camera-connecting-no-media-stalled"
current["first_media_committed"] = True
assert classify_camera_recording_health(base) is None
def test_backend_camera_watchdog_reports_stall_without_browser_polling(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("MISSIONCORE_FFMPEG_BINARY", str(_silent_ffmpeg(tmp_path)))
observed: list[tuple[str, int]] = []
wake = threading.Event()
def observe(reason: str, generation: int) -> None:
observed.append((reason, generation))
wake.set()
gateway = XgridsK1CameraGateway(
tmp_path,
XGRIDS_K1_PLUGIN_ID,
producer_stall_observer=observe,
producer_stall_milliseconds=30,
producer_watchdog_interval_seconds=0.01,
)
session = tmp_path / "sessions" / "camera-watchdog"
session.mkdir(parents=True)
try:
selected = gateway.select("sensor.camera.right", "192.168.1.20")
gateway.start_recording(session)
assert wake.wait(timeout=1.0) is True
assert observed == [
("camera-connecting-no-init-stalled", selected["generation"])
]
time.sleep(0.05)
assert len(observed) == 1
finally:
gateway.close()
def test_fragment_above_one_mib_is_durably_committed_and_media_ready(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
complete_fragment_bytes = 2 * 1024 * 1024
monkeypatch.setenv(
"MISSIONCORE_FFMPEG_BINARY",
str(
_single_fragment_ffmpeg(
tmp_path,
complete_fragment_bytes=complete_fragment_bytes,
)
),
)
gateway = XgridsK1CameraGateway(tmp_path, XGRIDS_K1_PLUGIN_ID)
session = tmp_path / "sessions" / "camera-large-valid-fragment"
session.mkdir(parents=True)
try:
selected = gateway.select("sensor.camera.right", "192.168.1.20")
gateway.start_recording(session)
_wait_until(lambda: gateway.snapshot()["recording"]["media_ready"] is True)
snapshot = gateway.snapshot()
current = snapshot["recording"]["current_epoch"]
assert current["generation"] == selected["generation"]
assert current["init_committed"] is True
assert current["first_media_committed"] is True
assert current["committed_media_segment_count"] == 1
assert isinstance(current["init_committed_age_ms"], int)
assert isinstance(current["first_media_committed_age_ms"], int)
segment = (
session
/ "media"
/ "sensor.camera.right"
/ f"epoch-{selected['generation']}"
/ "segments"
/ "1.m4s"
)
assert segment.stat().st_size == complete_fragment_bytes
finally:
gateway.close()
def test_fragment_above_eight_mib_fails_closed_with_size_only_diagnostic(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
observed_bytes = MAX_FMP4_SEGMENT_BYTES + 1
monkeypatch.setenv(
"MISSIONCORE_FFMPEG_BINARY",
str(
_single_fragment_ffmpeg(
tmp_path,
complete_fragment_bytes=observed_bytes,
)
),
)
gateway = XgridsK1CameraGateway(tmp_path, XGRIDS_K1_PLUGIN_ID)
session = tmp_path / "sessions" / "camera-oversized-fragment"
session.mkdir(parents=True)
try:
selected = gateway.select("sensor.camera.right", "192.168.1.20")
gateway.start_recording(session)
_wait_until(
lambda: (
gateway.snapshot()["phase"] == "error"
and gateway.snapshot()["recording"]["completed_epochs"] == 1
)
)
snapshot = gateway.snapshot()
assert snapshot["error"] == {
"code": "segment-too-large",
"message": "Camera adapter отклонил слишком большой video segment.",
"observed_size_bytes": observed_bytes,
"maximum_size_bytes": MAX_FMP4_SEGMENT_BYTES,
}
assert snapshot["recording"]["committed_media_segment_count"] == 0
serialized = json.dumps(snapshot)
assert "PRIVATE-FRAME-BYTES" not in serialized
epoch = (
session
/ "media"
/ "sensor.camera.right"
/ f"epoch-{selected['generation']}"
)
assert list((epoch / "segments").glob("*.m4s")) == []
summary = json.loads((epoch / "summary.json").read_text(encoding="utf-8"))
assert summary["failure_code"] == "segment-too-large"
finally:
gateway.close()
def test_gateway_emits_init_and_complete_media_segments_without_transcoding(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
gateway = _gateway(tmp_path, monkeypatch)
try:
state = gateway.select("sensor.camera.right", "192.168.1.20")
lease = gateway.open_delivery(state["generation"])
init_segment = lease.segments.get(timeout=3)
media_segment = lease.segments.get(timeout=3)
assert init_segment is not None and init_segment[0] == "init"
assert b"ftyp" in init_segment[1] and b"moov" in init_segment[1]
assert media_segment is not None and media_segment[0] == "media"
assert b"moof" in media_segment[1] and b"mdat" in media_segment[1]
gateway.mark_streaming(lease)
assert gateway.snapshot()["phase"] == "streaming"
gateway.release_delivery(lease, client_closed=True)
assert gateway.snapshot()["phase"] == "selected"
finally:
gateway.close()
def test_acquisition_records_without_browser_and_source_switch_seals_epochs(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
gateway = _gateway(tmp_path, monkeypatch)
session = tmp_path / "sessions" / "camera-acquisition"
try:
left = gateway.select("sensor.camera.left", "192.168.1.20")
with pytest.raises(ValueError, match="does not exist"):
gateway.start_recording(session)
assert session.exists() is False
session.mkdir(parents=True)
gateway.start_recording(session)
left_epoch = session / "media" / "sensor.camera.left" / f"epoch-{left['generation']}"
# No open_delivery/WebSocket exists: acquisition ownership alone starts
# FFmpeg and commits the init segment before any preview consumer.
_wait_until(
lambda: (
(left_epoch / "init.mp4").is_file()
and len(list((left_epoch / "segments").glob("*.m4s"))) == 1
)
)
_wait_until(lambda: gateway.snapshot()["phase"] == "streaming")
right = gateway.select("sensor.camera.right", "192.168.1.20")
right_epoch = session / "media" / "sensor.camera.right" / f"epoch-{right['generation']}"
_wait_until(lambda: gateway.snapshot()["phase"] == "streaming")
_wait_until(lambda: len(list((right_epoch / "segments").glob("*.m4s"))) == 1)
lease = gateway.open_delivery(right["generation"])
init_segment = lease.segments.get(timeout=1)
assert init_segment is not None and init_segment[0] == "init"
gateway.release_delivery(lease, client_closed=True)
# Browser disposal is not producer disposal while recording is active.
assert lease.process.poll() is None
assert gateway.snapshot()["recording"]["active"] is True
assert gateway.snapshot()["phase"] == "streaming"
stopped = gateway.stop_recording(status="complete")
assert stopped["recording"]["active"] is False
assert stopped["recording"]["completed_epochs"] == 2
summaries = []
for epoch in (left_epoch, right_epoch):
init = (epoch / "init.mp4").read_bytes()
entries = [
json.loads(line)
for line in (epoch / "index.jsonl").read_text(encoding="utf-8").splitlines()
]
summary = json.loads((epoch / "summary.json").read_text(encoding="utf-8"))
summaries.append(summary)
assert [entry["kind"] for entry in entries] == ["media"]
assert all(
entry["length"] == len((epoch / entry["path"]).read_bytes())
and hashlib.sha256((epoch / entry["path"]).read_bytes()).hexdigest()
== entry["sha256"]
for entry in entries
)
assert summary["status"] == "complete"
assert summary["segment_count"] == 1
assert summary["entry_count"] == 1
assert summary["valid_bytes"] == len(init) + sum(entry["length"] for entry in entries)
assert summaries[0]["failure_code"] == "source-switch"
assert summaries[1]["failure_code"] is None
serialized = json.dumps(stopped)
assert "192.168.1.20" not in serialized
assert "rtsp://" not in serialized
finally:
gateway.close()
def test_acquisition_allows_independent_browser_consumers_without_lease_churn(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""React/window replacement cannot starve the currently visible camera."""
gateway = _gateway(tmp_path, monkeypatch)
session = tmp_path / "sessions" / "camera-browser-consumers"
session.mkdir(parents=True)
try:
selected = gateway.select("sensor.camera.right", "192.168.1.20")
gateway.start_recording(session)
first = gateway.open_delivery(selected["generation"])
second = gateway.open_delivery(selected["generation"])
assert first.segments.get(timeout=3)[0] == "init"
assert second.segments.get(timeout=3)[0] == "init"
assert gateway.snapshot()["recording"]["preview_consumer_count"] == 2
gateway.release_delivery(first, client_closed=True)
assert gateway.snapshot()["recording"]["preview_consumer_count"] == 1
assert second.process.poll() is None
assert second.failure_code is None
gateway.release_delivery(second, client_closed=True)
state = gateway.snapshot()
assert state["recording"]["preview_consumer_count"] == 0
assert state["recording"]["active"] is True
assert state["recording"]["producer_alive"] is True
finally:
gateway.close()
def test_release_delivery_wakes_blocked_reader_without_stopping_recording(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
gateway = _gateway(tmp_path, monkeypatch)
session = tmp_path / "released-browser-reader"
session.mkdir(parents=True)
released: list[tuple[str, bytes] | None] = []
reader_started = threading.Event()
try:
selected = gateway.select("sensor.camera.right", "192.168.1.20")
gateway.start_recording(session)
lease = gateway.open_delivery(selected["generation"])
assert lease.segments.get(timeout=3)[0] == "init"
assert lease.segments.get(timeout=3)[0] == "media"
def wait_for_next_segment() -> None:
reader_started.set()
released.append(lease.segments.get())
reader = threading.Thread(target=wait_for_next_segment)
reader.start()
assert reader_started.wait(timeout=1)
time.sleep(0.02)
assert released == []
gateway.release_delivery(lease, client_closed=True)
reader.join(timeout=1)
assert reader.is_alive() is False
assert released == [None]
recording = gateway.snapshot()["recording"]
assert recording["active"] is True
assert recording["producer_alive"] is True
assert recording["preview_consumer_count"] == 0
finally:
gateway.close()
def test_preview_queue_enforces_exact_fragment_byte_and_age_bounds() -> None:
now = [10.0]
fragment_bounded = _CameraPreviewSegmentQueue(clock=lambda: now[0])
for index in range(MAX_CAMERA_PREVIEW_QUEUED_SEGMENTS):
assert fragment_bounded.offer(("media", bytes([index % 256]))) is True
assert fragment_bounded.offer(("media", b"overflow")) is False
byte_bounded = _CameraPreviewSegmentQueue(clock=lambda: now[0])
one_mebibyte = bytes(1024 * 1024)
for _ in range(MAX_CAMERA_PREVIEW_QUEUED_BYTES // len(one_mebibyte)):
assert byte_bounded.offer(("media", one_mebibyte)) is True
assert byte_bounded.queued_bytes == MAX_CAMERA_PREVIEW_QUEUED_BYTES
assert byte_bounded.offer(("media", b"x")) is False
valid_maximum = _CameraPreviewSegmentQueue()
assert valid_maximum.offer(("init", bytes(512 * 1024))) is True
assert valid_maximum.offer(("media", bytes(MAX_FMP4_SEGMENT_BYTES))) is True
assert valid_maximum.queued_bytes == MAX_FMP4_SEGMENT_BYTES + 512 * 1024
age_bounded = _CameraPreviewSegmentQueue(clock=lambda: now[0])
assert age_bounded.offer(("init", b"init")) is True
now[0] += MAX_CAMERA_PREVIEW_QUEUE_AGE_SECONDS
assert age_bounded.offer(("media", b"at-boundary")) is True
now[0] += 0.001
assert age_bounded.offer(("media", b"too-old")) is False
def test_preview_queue_survives_measured_ui_pause() -> None:
now = [100.0]
preview_queue = _CameraPreviewSegmentQueue(clock=lambda: now[0])
assert preview_queue.offer(("media", b"first")) is True
now[0] += 3.2
assert preview_queue.offer(("media", b"next")) is True
def test_camera_router_closes_lagging_preview_with_explicit_private_code(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
gateway = _gateway(tmp_path, monkeypatch)
session = tmp_path / "router-send-timeout"
session.mkdir()
selected = gateway.select("sensor.camera.right", "192.168.1.20")
gateway.start_recording(session)
_wait_until(lambda: gateway.snapshot()["recording"]["active_epoch"] is not None)
class SlowWebSocket:
def __init__(self) -> None:
self.accepted = False
self.close_code: int | None = None
self.close_reason: str | None = None
async def accept(self) -> None:
self.accepted = True
async def send_bytes(self, _payload: bytes) -> None:
await asyncio.sleep(CAMERA_PREVIEW_SEND_TIMEOUT_SECONDS * 4)
async def close(self, *, code: int = 1000, reason: str = "") -> None:
self.close_code = code
self.close_reason = reason
websocket = SlowWebSocket()
route = build_xgrids_k1_camera_router(gateway, XGRIDS_K1_PLUGIN_ID).routes[0]
monkeypatch.setattr(camera_module, "CAMERA_PREVIEW_SEND_TIMEOUT_SECONDS", 0.01)
try:
asyncio.run(route.endpoint(websocket, selected["generation"]))
assert websocket.accepted is True
assert websocket.close_code == 4008
assert websocket.close_reason == "preview-consumer-lagged"
recording = gateway.snapshot()["recording"]
assert recording["preview_consumer_count"] == 0
assert recording["producer_alive"] is True
finally:
gateway.close()
def test_preview_consumer_cardinality_is_bounded_and_newest_keeps_streaming(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("MISSIONCORE_FFMPEG_BINARY", str(_burst_ffmpeg(tmp_path)))
gateway = XgridsK1CameraGateway(tmp_path, XGRIDS_K1_PLUGIN_ID)
session = tmp_path / "sessions" / "bounded-browser-consumers"
session.mkdir(parents=True)
leases = []
try:
selected = gateway.select("sensor.camera.right", "192.168.1.20")
gateway.start_recording(session)
_wait_until(lambda: gateway.snapshot()["recording"]["active_epoch"] is not None)
for _ in range(MAX_CAMERA_PREVIEW_CONSUMERS + 3):
leases.append(gateway.open_delivery(selected["generation"]))
state = gateway.snapshot()
assert state["recording"]["preview_consumer_count"] == MAX_CAMERA_PREVIEW_CONSUMERS
assert all(lease.failure_code == "consumer-superseded" for lease in leases[:3])
newest = leases[-1]
assert newest.segments.get(timeout=3)[0] == "init"
assert newest.segments.get(timeout=3)[0] == "media"
assert newest.failure_code is None
assert newest.process.poll() is None
recording = gateway.snapshot()["recording"]
assert recording["producer_alive"] is True
assert recording["committed_media_segment_count"] >= 1
finally:
for lease in leases:
gateway.release_delivery(lease, client_closed=True)
gateway.close()
def test_acquisition_recovery_cas_restarts_stalled_ffmpeg_and_preserves_old_epoch(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
gateway = _gateway(tmp_path, monkeypatch)
session = tmp_path / "sessions" / "camera-recovery"
session.mkdir(parents=True)
try:
gateway.start_recording(session)
selected = gateway.select("sensor.camera.right", "192.168.1.20")
first_generation = selected["generation"]
first_epoch = session / "media" / "sensor.camera.right" / f"epoch-{first_generation}"
_wait_until(lambda: gateway.snapshot()["phase"] == "streaming")
_wait_until(lambda: (first_epoch / "init.mp4").is_file())
before = gateway.snapshot()
assert before["recording"]["producer_alive"] is True
assert before["recording"]["active_epoch"] == first_generation
restarted = gateway.restart_recording_producer(
"sensor.camera.right",
"192.168.1.20",
expected_generation=before["generation"],
expected_recording_session=session.name,
expected_active_epoch=before["recording"]["active_epoch"],
expected_recording_media_segment_count=before["recording"][
"committed_media_segment_count"
],
pre_detach_fence=lambda reserve: reserve(),
)
assert restarted["generation"] == first_generation + 1
assert restarted["recording"]["active_epoch"] == first_generation + 1
assert restarted["recording"]["completed_epochs"] == 1
first_summary = json.loads((first_epoch / "summary.json").read_text(encoding="utf-8"))
assert first_summary["status"] == "interrupted"
assert first_summary["failure_code"] == "active-stream-connection-recovery"
second_epoch = session / "media" / "sensor.camera.right" / f"epoch-{first_generation + 1}"
_wait_until(lambda: (second_epoch / "init.mp4").is_file())
with pytest.raises(ValueError, match="устарело|lineage"):
gateway.restart_recording_producer(
"sensor.camera.right",
"192.168.1.20",
expected_generation=before["generation"],
expected_recording_session=session.name,
expected_active_epoch=before["recording"]["active_epoch"],
expected_recording_media_segment_count=before["recording"][
"committed_media_segment_count"
],
pre_detach_fence=lambda reserve: reserve(),
)
assert gateway.snapshot()["generation"] == first_generation + 1
finally:
gateway.close()
def test_stop_first_pre_detach_denial_leaves_g7_and_archive_bytes_unchanged(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
gateway = _gateway(tmp_path, monkeypatch)
session = tmp_path / "sessions" / "camera-stop-first-reservation"
session.mkdir(parents=True)
try:
# Build a nontrivial lineage so the test proves the exact caller CAS,
# not an incidental initial-generation special case.
for source_id in (
"sensor.camera.right",
"sensor.camera.left",
"sensor.camera.right",
"sensor.camera.left",
"sensor.camera.right",
"sensor.camera.left",
"sensor.camera.right",
):
selected = gateway.select(source_id, "192.168.1.20")
assert selected["generation"] == 7
gateway.start_recording(session)
_wait_until(lambda: gateway.snapshot()["recording"]["media_ready"] is True)
before = gateway.snapshot()
before_recording = before["recording"]
before_files = {
path.relative_to(session).as_posix(): path.read_bytes()
for path in session.rglob("*")
if path.is_file()
}
with pytest.raises(ValueError, match="устарела|lineage"):
gateway.restart_recording_producer(
"sensor.camera.right",
"192.168.1.20",
expected_generation=7,
expected_recording_session=session.name,
expected_active_epoch=7,
expected_recording_media_segment_count=before_recording[
"committed_media_segment_count"
],
pre_detach_fence=lambda _reserve: False,
)
after = gateway.snapshot()
after_recording = after["recording"]
assert after["generation"] == before["generation"] == 7
assert after["revision"] == before["revision"]
assert after["phase"] == before["phase"] == "streaming"
assert after["error"] == before["error"] is None
assert after_recording["active_epoch"] == before_recording["active_epoch"] == 7
assert after_recording["producer_alive"] is True
assert after_recording["completed_epochs"] == before_recording["completed_epochs"]
assert after_recording["committed_media_segment_count"] == before_recording[
"committed_media_segment_count"
]
assert {
path.relative_to(session).as_posix(): path.read_bytes()
for path in session.rglob("*")
if path.is_file()
} == before_files
finally:
gateway.close()
def test_fresh_durable_media_supersedes_stale_watchdog_restart_cas(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
release_second_media = tmp_path / "release-second-camera-media"
monkeypatch.setenv(
"MISSIONCORE_FFMPEG_BINARY",
str(
_gated_media_progress_ffmpeg(
tmp_path,
release_second_media=release_second_media,
)
),
)
session = tmp_path / "sessions" / "camera-progress-cas"
session.mkdir(parents=True)
watchdog_snapshotted = threading.Event()
allow_stale_cas = threading.Event()
cas_done = threading.Event()
cas_rejections: list[str] = []
observer_errors: list[Exception] = []
gateway: XgridsK1CameraGateway | None = None
def backend_watchdog(reason: str, generation: int) -> None:
if reason != "camera-stream-stalled":
return
try:
assert gateway is not None
stale = gateway.snapshot()
stale_recording = stale["recording"]
assert stale_recording["committed_media_segment_count"] == 1
watchdog_snapshotted.set()
assert allow_stale_cas.wait(timeout=3)
try:
gateway.restart_recording_producer(
"sensor.camera.right",
"192.168.1.20",
expected_generation=generation,
expected_recording_session=session.name,
expected_active_epoch=stale_recording["active_epoch"],
expected_recording_media_segment_count=stale_recording[
"committed_media_segment_count"
],
pre_detach_fence=lambda reserve: reserve(),
)
except ValueError as exc:
cas_rejections.append(str(exc))
else:
raise AssertionError("fresh durable media must supersede stale camera CAS")
except Exception as exc:
observer_errors.append(exc)
finally:
cas_done.set()
gateway = XgridsK1CameraGateway(
tmp_path,
XGRIDS_K1_PLUGIN_ID,
producer_stall_observer=backend_watchdog,
producer_stall_milliseconds=40,
producer_watchdog_interval_seconds=0.01,
)
try:
selected = gateway.select("sensor.camera.right", "192.168.1.20")
gateway.start_recording(session)
_wait_until(
lambda: gateway is not None
and gateway.snapshot()["recording"]["committed_media_segment_count"] == 1
)
assert watchdog_snapshotted.wait(timeout=3)
# This archive append lands after the watchdog's health snapshot but
# before its lifecycle-CAS entry. Generation and active epoch remain
# unchanged; only the durable progress token can fence the stale restart.
release_second_media.write_text("release", encoding="utf-8")
_wait_until(
lambda: gateway is not None
and gateway.snapshot()["recording"]["committed_media_segment_count"] == 2
)
allow_stale_cas.set()
assert cas_done.wait(timeout=3)
current = gateway.snapshot()
assert observer_errors == []
assert len(cas_rejections) == 1
assert "lineage" in cas_rejections[0]
assert current["generation"] == selected["generation"]
assert current["phase"] == "streaming"
assert current["error"] is None
assert current["recording"]["producer_alive"] is True
assert current["recording"]["active_epoch"] == selected["generation"]
assert current["recording"]["committed_media_segment_count"] == 2
assert current["recording"]["completed_epochs"] == 0
epoch = (
session
/ "media"
/ "sensor.camera.right"
/ f"epoch-{selected['generation']}"
)
assert len(list((epoch / "segments").glob("*.m4s"))) == 2
finally:
allow_stale_cas.set()
gateway.close()
def test_partial_same_session_activation_can_retry_exact_local_producer(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
gateway = _gateway(tmp_path, monkeypatch)
session = tmp_path / "sessions" / "camera-partial-activation"
session.mkdir(parents=True)
real_popen = camera_module.subprocess.Popen
spawn_attempts = 0
def fail_first_popen(*args: object, **kwargs: object) -> object:
nonlocal spawn_attempts
spawn_attempts += 1
if spawn_attempts == 1:
raise OSError("synthetic first Popen failure")
return real_popen(*args, **kwargs)
monkeypatch.setattr(camera_module.subprocess, "Popen", fail_first_popen)
try:
gateway.select("sensor.camera.right", "192.168.1.20")
with pytest.raises(RuntimeError, match="camera adapter"):
gateway.start_recording(session)
partial = gateway.snapshot()
assert partial["phase"] == "error"
assert partial["active_source_id"] == "sensor.camera.right"
assert partial["recording"]["active"] is True
assert partial["recording"]["session"] == session.name
assert partial["recording"]["active_epoch"] is None
failed_epoch = session / "media" / "sensor.camera.right" / f"epoch-{partial['generation']}"
failed_summary = json.loads((failed_epoch / "summary.json").read_text(encoding="utf-8"))
assert failed_summary["status"] == "failed"
assert failed_summary["failure_code"] == "ffmpeg-start-failed"
retried = gateway.retry_recording_producer(
"sensor.camera.right",
"192.168.1.20",
expected_generation=partial["generation"],
expected_recording_session=session.name,
pre_retry_fence=lambda reserve: reserve(),
)
assert spawn_attempts == 2
assert retried["phase"] in {"connecting", "streaming"}
assert retried["generation"] == partial["generation"] + 1
assert retried["recording"]["active_epoch"] == partial["generation"] + 1
assert retried["recording"]["producer_alive"] is True
assert json.loads((failed_epoch / "summary.json").read_text(encoding="utf-8")) == (
failed_summary
)
with pytest.raises(ValueError, match="lineage"):
gateway.retry_recording_producer(
"sensor.camera.right",
"192.168.1.20",
expected_generation=partial["generation"],
expected_recording_session=session.name,
pre_retry_fence=lambda reserve: reserve(),
)
finally:
gateway.close()
def test_expected_camera_source_end_during_device_stop_seals_complete_epoch(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv(
"MISSIONCORE_FFMPEG_BINARY",
str(_clean_source_end_ffmpeg(tmp_path)),
)
gateway = XgridsK1CameraGateway(tmp_path, XGRIDS_K1_PLUGIN_ID)
session = tmp_path / "session"
session.mkdir()
try:
selected = gateway.select("sensor.camera.left", "192.168.1.20")
gateway.start_recording(session)
gateway.expect_source_end_for_device_stop()
_wait_until(
lambda: gateway.snapshot()["recording"]["completed_epochs"] == 1,
)
state = gateway.snapshot()
assert state["phase"] == "idle"
assert state["error"] is None
assert state["delivery"] is None
assert state["recording"]["active"] is True
assert state["recording"]["source_end_expected"] is False
gateway.stop_recording(status="complete")
epoch = session / "media" / "sensor.camera.left" / f"epoch-{selected['generation']}"
summary = json.loads((epoch / "summary.json").read_text(encoding="utf-8"))
assert summary["status"] == "complete"
assert summary["failure_code"] is None
assert summary["media_segment_count"] == 1
finally:
gateway.close()
def test_unexpected_camera_source_end_remains_a_recording_failure(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv(
"MISSIONCORE_FFMPEG_BINARY",
str(_clean_source_end_ffmpeg(tmp_path)),
)
gateway = XgridsK1CameraGateway(tmp_path, XGRIDS_K1_PLUGIN_ID)
session = tmp_path / "session"
session.mkdir()
try:
selected = gateway.select("sensor.camera.left", "192.168.1.20")
gateway.start_recording(session)
_wait_until(
lambda: gateway.snapshot()["recording"]["completed_epochs"] == 1,
)
state = gateway.snapshot()
assert state["phase"] == "error"
assert state["error"]["code"] == "camera-source-ended"
epoch = session / "media" / "sensor.camera.left" / f"epoch-{selected['generation']}"
summary = json.loads((epoch / "summary.json").read_text(encoding="utf-8"))
assert summary["status"] == "interrupted"
assert summary["failure_code"] == "camera-source-ended"
finally:
gateway.close()
def test_browser_reconnect_cannot_respawn_sealed_epoch_before_backend_cas(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
executable, invocation_count = _recoverable_source_end_ffmpeg(tmp_path)
monkeypatch.setenv("MISSIONCORE_FFMPEG_BINARY", str(executable))
session = tmp_path / "sessions" / "camera-browser-backend-recovery-race"
session.mkdir(parents=True)
watchdog_entered = threading.Event()
allow_backend_cas = threading.Event()
backend_cas_done = threading.Event()
observer_errors: list[Exception] = []
recovered_snapshots: list[dict[str, object]] = []
gateway: XgridsK1CameraGateway | None = None
def backend_watchdog(reason: str, generation: int) -> None:
try:
assert reason == "camera-source-ended"
watchdog_entered.set()
assert allow_backend_cas.wait(timeout=3)
assert gateway is not None
recovered_snapshots.append(
gateway.restart_recording_producer(
"sensor.camera.right",
"192.168.1.20",
expected_generation=generation,
expected_recording_session=session.name,
expected_active_epoch=None,
expected_recording_media_segment_count=1,
pre_detach_fence=lambda reserve: reserve(),
)
)
except Exception as exc:
observer_errors.append(exc)
finally:
backend_cas_done.set()
gateway = XgridsK1CameraGateway(
tmp_path,
XGRIDS_K1_PLUGIN_ID,
producer_stall_observer=backend_watchdog,
)
try:
selected = gateway.select("sensor.camera.right", "192.168.1.20")
first_generation = selected["generation"]
gateway.start_recording(session)
assert watchdog_entered.wait(timeout=3)
sealed = gateway.snapshot()
first_epoch = (
session
/ "media"
/ "sensor.camera.right"
/ f"epoch-{first_generation}"
)
first_summary = json.loads(
(first_epoch / "summary.json").read_text(encoding="utf-8")
)
assert invocation_count.read_text(encoding="utf-8") == "1"
assert sealed["generation"] == first_generation
assert sealed["phase"] == "error"
assert sealed["error"]["code"] == "camera-source-ended"
assert sealed["recording"]["producer_alive"] is False
assert sealed["recording"]["active_epoch"] is None
assert sealed["recording"]["completed_epochs"] == 1
# A browser reconnect reaches the sealed generation before the backend
# observer is allowed to take its CAS. It must neither spawn FFmpeg nor
# reopen the finalized epoch or misclassify the collision as storage.
with pytest.raises(RuntimeError, match="backend recovery"):
gateway.open_delivery(first_generation)
after_browser = gateway.snapshot()
assert invocation_count.read_text(encoding="utf-8") == "1"
assert after_browser["generation"] == first_generation
assert after_browser["recording"]["producer_alive"] is False
assert after_browser["recording"]["active_epoch"] is None
assert after_browser["recording"]["completed_epochs"] == 1
assert after_browser["error"]["code"] == "camera-source-ended"
assert list((first_epoch.parent).glob("epoch-*")) == [first_epoch]
assert json.loads(
(first_epoch / "summary.json").read_text(encoding="utf-8")
) == first_summary
allow_backend_cas.set()
assert backend_cas_done.wait(timeout=3)
assert observer_errors == []
_wait_until(
lambda: (
gateway is not None
and gateway.snapshot()["generation"] == first_generation + 1
and gateway.snapshot()["recording"]["media_ready"] is True
)
)
recovered = gateway.snapshot()
assert len(recovered_snapshots) == 1
assert invocation_count.read_text(encoding="utf-8") == "2"
assert recovered["generation"] == first_generation + 1
assert recovered["phase"] == "streaming"
assert recovered["error"] is None
assert recovered["recording"]["active_epoch"] == first_generation + 1
assert recovered["recording"]["media_ready"] is True
assert recovered["recording"]["current_epoch"]["generation"] == (
first_generation + 1
)
assert json.loads(
(first_epoch / "summary.json").read_text(encoding="utf-8")
) == first_summary
finally:
allow_backend_cas.set()
gateway.close()
def test_source_end_none_epoch_cas_waits_for_canonical_archive_seal(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
executable, _invocation_count = _recoverable_source_end_ffmpeg(tmp_path)
monkeypatch.setenv("MISSIONCORE_FFMPEG_BINARY", str(executable))
gateway = XgridsK1CameraGateway(tmp_path, XGRIDS_K1_PLUGIN_ID)
session = tmp_path / "sessions" / "camera-source-end-seal-fence"
session.mkdir(parents=True)
finalize_entered = threading.Event()
release_finalize = threading.Event()
real_finalize = gateway._finalize_archive # noqa: SLF001
def blocked_finalize(*args: object, **kwargs: object) -> None:
finalize_entered.set()
assert release_finalize.wait(timeout=3)
real_finalize(*args, **kwargs) # type: ignore[arg-type]
monkeypatch.setattr(gateway, "_finalize_archive", blocked_finalize)
try:
selected = gateway.select("sensor.camera.right", "192.168.1.20")
gateway.start_recording(session)
assert finalize_entered.wait(timeout=3)
unsealed = gateway.snapshot()
assert unsealed["phase"] == "error"
assert unsealed["error"]["code"] == "camera-source-ended"
assert unsealed["recording"]["active_epoch"] is None
assert unsealed["recording"]["completed_epochs"] == 0
with pytest.raises(ValueError, match="устарела|lineage"):
gateway.restart_recording_producer(
"sensor.camera.right",
"192.168.1.20",
expected_generation=selected["generation"],
expected_recording_session=session.name,
expected_active_epoch=None,
expected_recording_media_segment_count=1,
pre_detach_fence=lambda reserve: reserve(),
)
denied = gateway.snapshot()
assert denied["generation"] == selected["generation"]
assert denied["recording"]["completed_epochs"] == 0
release_finalize.set()
_wait_until(lambda: gateway.snapshot()["recording"]["completed_epochs"] == 1)
sealed = gateway.snapshot()
summary = sealed["recording"]["last_summary"]
assert summary["schema_version"] == "missioncore.camera-recording/v1"
assert summary["failure_code"] == "camera-source-ended"
restarted = gateway.restart_recording_producer(
"sensor.camera.right",
"192.168.1.20",
expected_generation=selected["generation"],
expected_recording_session=session.name,
expected_active_epoch=None,
expected_recording_media_segment_count=1,
pre_detach_fence=lambda reserve: reserve(),
)
assert restarted["generation"] == selected["generation"] + 1
finally:
release_finalize.set()
gateway.close()
def test_acquisition_candidate_never_spawns_before_authority_reservation(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("MISSIONCORE_FFMPEG_BINARY", str(_silent_ffmpeg(tmp_path)))
session = tmp_path / "sessions" / "camera-pre-popen-denied"
session.mkdir(parents=True)
gateway = XgridsK1CameraGateway(tmp_path, XGRIDS_K1_PLUGIN_ID)
popen_called = False
def forbidden_popen(*_: object, **__: object) -> object:
nonlocal popen_called
popen_called = True
raise AssertionError("FFmpeg must not start before authority reservation")
monkeypatch.setattr(camera_module.subprocess, "Popen", forbidden_popen)
try:
with pytest.raises(ValueError, match="authority"):
gateway.activate_recording_producer(
"sensor.camera.right",
"192.168.1.20",
session,
pre_prepare_fence=lambda _reserve: False,
commit_fence=lambda commit: commit(),
)
snapshot = gateway.snapshot()
assert popen_called is False
assert snapshot["active_source_id"] is None
assert snapshot["recording"]["active"] is False
finally:
gateway.close()
def test_acquisition_candidate_binds_epoch_before_any_reader_thread_starts(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
monkeypatch.setenv("MISSIONCORE_FFMPEG_BINARY", str(_silent_ffmpeg(tmp_path)))
session = tmp_path / "sessions" / "camera-bind-before-reader"
session.mkdir(parents=True)
gateway = XgridsK1CameraGateway(tmp_path, XGRIDS_K1_PLUGIN_ID)
caplog.set_level("INFO", logger="k1link.device_plugins.xgrids_k1.camera")
bound_generation: list[int] = []
reader_start_generation: list[int] = []
authority_reserved = False
def reserve_authority(reserve: Callable[[], bool]) -> bool:
nonlocal authority_reserved
assert authority_reserved is False
assert reserve() is True
authority_reserved = True
return True
def bind(committed: dict[str, object]) -> None:
recording = committed["recording"]
assert isinstance(recording, dict)
generation = recording["active_epoch"]
assert isinstance(generation, int)
bound_generation.append(generation)
def start_threads(producer: object) -> None:
generation = producer.generation # type: ignore[attr-defined]
assert authority_reserved is True
assert bound_generation == [generation]
reader_start_generation.append(generation)
monkeypatch.setattr(gateway, "_start_producer_threads", start_threads)
try:
activated = gateway.activate_recording_producer(
"sensor.camera.right",
"192.168.1.20",
session,
pre_prepare_fence=reserve_authority,
commit_fence=lambda commit: commit(),
committed_before_start=bind,
)
assert bound_generation == [activated["recording"]["active_epoch"]]
assert reader_start_generation == bound_generation
assert activated["recording"]["media_ready"] is False
timing = [
record
for record in caplog.records
if getattr(record, "event_code", None) == "k1_camera_activation_timing"
]
assert len(timing) == 1
assert timing[0].camera_generation == bound_generation[0]
assert timing[0].camera_authority_wait_ms >= 0
assert timing[0].camera_ffmpeg_prepare_ms >= 0
assert timing[0].camera_post_spawn_commit_ms >= 0
finally:
gateway.close()
def test_camera_storage_open_failure_is_loud_and_never_starts_ffmpeg(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
gateway = _gateway(tmp_path, monkeypatch)
session = tmp_path / "session"
session.mkdir()
popen_called = False
class FailingArchive:
def __init__(self, *_: object, **__: object) -> None:
raise camera_module.CameraArchiveError("synthetic storage failure")
def forbidden_popen(*_: object, **__: object) -> object:
nonlocal popen_called
popen_called = True
raise AssertionError("FFmpeg must not start without durable storage")
monkeypatch.setattr(camera_module, "CameraArchiveWriter", FailingArchive)
monkeypatch.setattr(camera_module.subprocess, "Popen", forbidden_popen)
try:
gateway.select("sensor.camera.left", "192.168.1.20")
with pytest.raises(RuntimeError, match="хранилище"):
gateway.start_recording(session)
state = gateway.snapshot()
assert popen_called is False
assert state["phase"] == "error"
assert state["error"]["code"] == "camera-storage-failed"
assert "192.168.1.20" not in json.dumps(state)
finally:
gateway.close()
def test_camera_storage_append_failure_fails_producer_and_epoch_loudly(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
original_append = camera_module.CameraArchiveWriter.append
def failing_append(
writer: object,
kind: object,
payload: bytes,
**timestamps: object,
) -> dict[str, object]:
if kind == "media":
raise camera_module.CameraArchiveError("synthetic media commit failure")
return original_append(writer, kind, payload, **timestamps) # type: ignore[arg-type]
monkeypatch.setattr(camera_module.CameraArchiveWriter, "append", failing_append)
gateway = _gateway(tmp_path, monkeypatch)
session = tmp_path / "session"
session.mkdir()
try:
selected = gateway.select("sensor.camera.left", "192.168.1.20")
gateway.start_recording(session)
_wait_until(lambda: gateway.snapshot()["phase"] == "error")
state = gateway.snapshot()
assert state["error"]["code"] == "camera-storage-failed"
summary_path = (
session
/ "media"
/ "sensor.camera.left"
/ f"epoch-{selected['generation']}"
/ "summary.json"
)
_wait_until(
lambda: (
summary_path.is_file()
and json.loads(summary_path.read_text(encoding="utf-8"))["status"] == "failed"
)
)
summary = json.loads(summary_path.read_text(encoding="utf-8"))
assert summary["status"] == "failed"
assert summary["failure_code"] == "camera-storage-failed"
assert "192.168.1.20" not in json.dumps(state)
finally:
gateway.close()
def test_routine_browser_stall_is_buffered_without_dropping_live_delivery(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
fragment_count = 40
monkeypatch.setenv(
"MISSIONCORE_FFMPEG_BINARY",
str(
_preview_buffer_ffmpeg(
tmp_path,
fragments=fragment_count,
payload_bytes=80 * 1024,
)
),
)
gateway = XgridsK1CameraGateway(tmp_path, XGRIDS_K1_PLUGIN_ID)
session = tmp_path / "routine-preview-stall"
session.mkdir()
try:
selected = gateway.select("sensor.camera.left", "192.168.1.20")
gateway.start_recording(session)
lease = gateway.open_delivery(selected["generation"])
# A busy Rerun/browser main thread can defer this WebSocket task for
# several seconds. That is ordinary scheduling jitter, not a dead
# consumer, and must not terminate the visible camera transport.
_wait_until(
lambda: (
gateway.snapshot()["recording"]["committed_media_segment_count"] == fragment_count
),
)
assert lease.failure_code is None
assert gateway.snapshot()["recording"]["preview_consumer_count"] == 1
assert lease.segments.queued_segments == fragment_count + 1
assert lease.segments.queued_bytes < MAX_CAMERA_PREVIEW_QUEUED_BYTES
assert lease.segments.get(timeout=1)[0] == "init"
for _ in range(fragment_count):
assert lease.segments.get(timeout=1)[0] == "media"
assert lease.segments.queued_segments == 0
assert lease.segments.queued_bytes == 0
finally:
gateway.close()
def test_permanently_slow_browser_is_dropped_without_stopping_archive_producer(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
fragment_count = MAX_CAMERA_PREVIEW_QUEUED_SEGMENTS + 20
monkeypatch.setenv(
"MISSIONCORE_FFMPEG_BINARY",
str(
_preview_buffer_ffmpeg(
tmp_path,
fragments=fragment_count,
payload_bytes=160 * 1024,
interval_seconds=0.02,
)
),
)
gateway = XgridsK1CameraGateway(tmp_path, XGRIDS_K1_PLUGIN_ID)
session = tmp_path / "session"
session.mkdir()
try:
selected = gateway.select("sensor.camera.left", "192.168.1.20")
gateway.start_recording(session)
lease = gateway.open_delivery(selected["generation"])
fast = gateway.open_delivery(selected["generation"])
producer_pid = fast.process.pid
active_epoch = gateway.snapshot()["recording"]["active_epoch"]
fast_segments: list[str] = []
def drain_fast_reader() -> None:
for _ in range(fragment_count + 1):
segment = fast.segments.get(timeout=3)
assert segment is not None
fast_segments.append(segment[0])
fast_reader = threading.Thread(target=drain_fast_reader)
fast_reader.start()
# Deliberately never drain the bounded queue.
_wait_until(lambda: lease.failure_code == "consumer-too-slow")
_wait_until(lambda: gateway.snapshot()["phase"] == "streaming")
assert lease.process.poll() is None
assert gateway.snapshot()["recording"]["active"] is True
replacement = gateway.open_delivery(selected["generation"])
assert replacement.segments.get(timeout=1)[0] == "init"
assert replacement.segments.get(timeout=3)[0] == "media"
assert replacement.process.pid == producer_pid
assert gateway.snapshot()["generation"] == selected["generation"]
assert gateway.snapshot()["recording"]["active_epoch"] == active_epoch
gateway.release_delivery(lease, client_closed=False)
epoch = session / "media" / "sensor.camera.left" / f"epoch-{selected['generation']}"
_wait_until(
lambda: len(list((epoch / "segments").glob("*.m4s"))) == fragment_count,
)
fast_reader.join(timeout=3)
assert fast_reader.is_alive() is False
assert fast_segments == ["init", *("media" for _ in range(fragment_count))]
assert fast.failure_code is None
gateway.release_delivery(fast, client_closed=True)
gateway.release_delivery(replacement, client_closed=True)
gateway.stop_recording(status="complete")
summary_path = epoch / "summary.json"
summary = json.loads(summary_path.read_text(encoding="utf-8"))
assert summary["status"] == "complete"
assert summary["segment_count"] == fragment_count
assert summary["media_segment_count"] == fragment_count
finally:
gateway.close()
def test_clean_stop_drains_ffmpeg_stdout_before_sealing_archive(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
fragment_count = 100
sentinel = tmp_path / "ffmpeg-wrote-buffered-tail"
monkeypatch.setenv(
"MISSIONCORE_FFMPEG_BINARY",
str(_buffered_tail_ffmpeg(tmp_path, sentinel, fragments=fragment_count)),
)
gateway = XgridsK1CameraGateway(tmp_path, XGRIDS_K1_PLUGIN_ID)
session = tmp_path / "session"
session.mkdir()
try:
selected = gateway.select("sensor.camera.left", "192.168.1.20")
gateway.start_recording(session)
_wait_until(sentinel.is_file)
gateway.stop_recording(status="complete")
epoch = session / "media" / "sensor.camera.left" / f"epoch-{selected['generation']}"
summary = json.loads((epoch / "summary.json").read_text(encoding="utf-8"))
assert summary["status"] == "complete"
assert summary["segment_count"] == fragment_count
assert len(list((epoch / "segments").glob("*.m4s"))) == fragment_count
assert len((epoch / "index.jsonl").read_text(encoding="utf-8").splitlines()) == (
fragment_count
)
finally:
gateway.close()
def test_clean_stop_marks_incomplete_fragment_tail_interrupted(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
sentinel = tmp_path / "ffmpeg-wrote-incomplete-tail"
monkeypatch.setenv(
"MISSIONCORE_FFMPEG_BINARY",
str(_incomplete_tail_ffmpeg(tmp_path, sentinel)),
)
gateway = XgridsK1CameraGateway(tmp_path, XGRIDS_K1_PLUGIN_ID)
session = tmp_path / "session"
session.mkdir()
try:
selected = gateway.select("sensor.camera.left", "192.168.1.20")
gateway.start_recording(session)
_wait_until(sentinel.is_file)
gateway.stop_recording(status="complete")
summary_path = (
session
/ "media"
/ "sensor.camera.left"
/ f"epoch-{selected['generation']}"
/ "summary.json"
)
summary = json.loads(summary_path.read_text(encoding="utf-8"))
assert summary["status"] == "interrupted"
assert summary["failure_code"] == "incomplete-fmp4-fragment"
assert summary["segment_count"] == 0
state = gateway.snapshot()
assert state["phase"] == "error"
assert state["error"]["code"] == "incomplete-fmp4-fragment"
finally:
gateway.close()
def test_service_publishes_two_dynamic_camera_rows_and_stale_stop_is_safe(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("MISSIONCORE_FFMPEG_BINARY", str(_fake_ffmpeg(tmp_path)))
service = XgridsK1CompatibilityService(tmp_path)
try:
with service._lock:
service._selected_device_id = "ble-transport-test"
service._k1_ip = "192.168.1.20"
service._connection_mode = "bridge"
service._device_id = "device-k1-test"
service._device_session_id = "device-session-test"
service._device_session_opened_at = "2026-07-16T20:00:00Z"
service._compatibility_attestation = {
"firmware_version": "3.0.2",
"topology": "direct-lan",
"verification": "live-device-info",
"basis": "selected-profile-live-device-info-required",
"observed_at": "2026-07-16T20:00:00Z",
}
supervisor = service._connection_supervisor # noqa: SLF001
target = EndpointTarget("192.168.1.20")
supervisor.set_intent(
intent_id="camera-test-intent",
requested_mode="bridge",
)
assert supervisor.observe_device_network_applied(
intent_id="camera-test-intent",
transport_ref="ble-transport-test",
connection_mode="bridge",
target=target,
source="ble-read-only-status",
)
host_epoch = supervisor.observe_host_path(
HostPathProbeResult(
available=True,
fingerprint="camera-test-route",
interface="en0",
source_ipv4="192.168.1.10",
route_class="direct",
)
)
assert supervisor.observe_endpoint(
target=target,
intent_id="camera-test-intent",
host_path_epoch=host_epoch,
reachable=True,
)
assert supervisor.observe_control_evidence(
VerifiedControlEvidence(
intent_id="camera-test-intent",
transport_ref="ble-transport-test",
host_path_epoch=host_epoch,
target=target,
connection_mode="bridge",
logical_device_id="device-k1-test",
compatibility_profile_id=XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
control_session_id="camera-test-control-session",
)
)
# This bounded gateway test supplies an explicit supervisor proof
# instead of opening the real MQTT application dialogue. Prevent the
# ordinary state reducer from correctly expiring that synthetic proof
# when it observes the deliberately idle test control session.
monkeypatch.setattr(
service,
"_reconcile_connection_supervisor",
lambda *_args, **_kwargs: None,
)
state = service.select_camera_preview(
CameraPreviewSelectRequest(
source_id="sensor.camera.left",
device_session_id="device-session-test",
)
)
cameras = [
stream
for stream in state["sensor_catalog"]["streams"]
if stream.get("semantic_channel_id") == "camera.preview.live"
]
assert len(cameras) == 2
assert sum(bool(stream["activation"]["selected"]) for stream in cameras) == 1
assert sum(stream["delivery"] is not None for stream in cameras) == 1
assert state["compatibility"]["profile_id"] == XGRIDS_K1_COMPATIBILITY_PROFILE_ID
camera_contract = json.dumps(
{"camera_preview": state["camera_preview"], "streams": cameras}
)
assert "rtsp://" not in camera_contract
assert "192.168.1.20" not in camera_contract
generation = state["camera_preview"]["generation"]
stopped = service.stop_camera_preview(
CameraPreviewStopRequest(
device_session_id="device-session-test",
generation=generation,
)
)
assert stopped["camera_preview"]["phase"] == "idle"
with pytest.raises(ValueError, match="device-сессия"):
service.select_camera_preview(
CameraPreviewSelectRequest(
source_id="sensor.camera.right",
device_session_id="stale-session",
)
)
finally:
service.close()