Route live recording through ffmpeg MP4 segments and the 5.15 batch subprocess, remove simulated RTSP chain 2, purge expired slices on startup and hourly, and expose TTL settings to the demo client. Co-authored-by: Cursor <cursoragent@cursor.com>
117 lines
3.4 KiB
Python
117 lines
3.4 KiB
Python
"""术间绑定与站点配置(OR_SITE_CONFIG_JSON)解析。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
from app.config import Settings
|
||
from app.or_site_config import (
|
||
load_or_site_config_from_path,
|
||
parse_or_site_config_object,
|
||
)
|
||
from app.services.voice_terminal_binding import VoiceTerminalBindingIndex
|
||
|
||
|
||
def test_parse_and_resolve() -> None:
|
||
idx = VoiceTerminalBindingIndex.from_binding_list(
|
||
[
|
||
{
|
||
"or_room_id": "OR-05",
|
||
"camera_ids": ["or-cam-01", "or-cam-02"],
|
||
"voice_terminal_id": "T-05",
|
||
}
|
||
]
|
||
)
|
||
assert idx is not None
|
||
assert idx.resolve_terminal(["or-cam-02", "or-cam-01"]) == "T-05"
|
||
assert idx.resolve_terminal(["or-cam-01"]) == "T-05"
|
||
|
||
|
||
def test_resolve_subset_uses_smallest_superset_when_ambiguous() -> None:
|
||
idx = VoiceTerminalBindingIndex.from_binding_list(
|
||
[
|
||
{
|
||
"or_room_id": "A",
|
||
"camera_ids": ["c1", "c2"],
|
||
"voice_terminal_id": "T-AB",
|
||
},
|
||
{
|
||
"or_room_id": "B",
|
||
"camera_ids": ["c1", "c2", "c3"],
|
||
"voice_terminal_id": "T-ABC",
|
||
},
|
||
]
|
||
)
|
||
assert idx is not None
|
||
assert idx.resolve_terminal(["c1"]) == "T-AB"
|
||
|
||
|
||
def test_duplicate_terminal_rejected() -> None:
|
||
assert (
|
||
VoiceTerminalBindingIndex.from_binding_list(
|
||
[
|
||
{"or_room_id": "A", "camera_ids": ["c1"], "voice_terminal_id": "T"},
|
||
{"or_room_id": "B", "camera_ids": ["c2"], "voice_terminal_id": "T"},
|
||
]
|
||
)
|
||
is None
|
||
)
|
||
|
||
|
||
def test_duplicate_camera_set_rejected() -> None:
|
||
assert (
|
||
VoiceTerminalBindingIndex.from_binding_list(
|
||
[
|
||
{"or_room_id": "A", "camera_ids": ["c1"], "voice_terminal_id": "T1"},
|
||
{"or_room_id": "B", "camera_ids": ["c1"], "voice_terminal_id": "T2"},
|
||
]
|
||
)
|
||
is None
|
||
)
|
||
|
||
|
||
def test_from_binding_list_empty() -> None:
|
||
idx = VoiceTerminalBindingIndex.from_binding_list([])
|
||
assert idx is not None
|
||
assert idx.resolve_terminal(["x"]) is None
|
||
|
||
|
||
def test_parse_site_config_rejects_unknown_keys() -> None:
|
||
with pytest.raises(ValueError, match="unknown top-level"):
|
||
parse_or_site_config_object(
|
||
{
|
||
"video_rtsp_urls": {},
|
||
"voice_or_room_bindings": [],
|
||
"extra": 1,
|
||
}
|
||
)
|
||
|
||
|
||
def test_parse_site_config_requires_both_sections() -> None:
|
||
with pytest.raises(ValueError, match="must include"):
|
||
parse_or_site_config_object({"video_rtsp_urls": {}})
|
||
|
||
|
||
def test_load_from_path_roundtrip(tmp_path: Path) -> None:
|
||
p = tmp_path / "site.json"
|
||
p.write_text(
|
||
'{"video_rtsp_urls":{"c1":"rtsp://h"},"voice_or_room_bindings":['
|
||
'{"or_room_id":"R","camera_ids":["a"],"voice_terminal_id":"T"}]}',
|
||
encoding="utf-8",
|
||
)
|
||
cfg = load_or_site_config_from_path(p)
|
||
assert cfg.video_rtsp_urls == {"c1": "rtsp://h"}
|
||
assert cfg.voice_bindings.resolve_terminal(["a"]) == "T"
|
||
|
||
|
||
def test_settings_video_map(tmp_path: Path) -> None:
|
||
p = tmp_path / "site.json"
|
||
p.write_text(
|
||
'{"video_rtsp_urls":{"c1":"rtsp://x"},"voice_or_room_bindings":[]}',
|
||
encoding="utf-8",
|
||
)
|
||
s = Settings(or_site_config_json_file=str(p))
|
||
assert s.video_rtsp_url_map() == {"c1": "rtsp://x"}
|