Files
operating-room-monitor-server/tests/test_voice_terminal_binding.py
Kevin 6b3adb4ad8 feat: 站点 JSON、语音终端 WebSocket 指派与客户端联调
- 用 OR_SITE_CONFIG_JSON_FILE 统一术间配置(video_rtsp_urls + voice_or_room_bindings)
- VoiceTerminalHub:assignment、WS 推送与 HTTP 查询;开录/停录后 notify
- 一键联调 orchestrate-and-start 与 /client/surgeries/start 共用指派逻辑,修复 demo 路径不发 WS
- 语音桌面端:SIGINT 退出、shutdown 清理、仅 WS 指派、固定 pending 轮询间隔、界面仅保留录音时长
- 新增/调整契约与绑定测试,文档与示例配置同步

Made-with: Cursor
2026-04-27 11:21:16 +08:00

135 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""术间绑定与站点配置OR_SITE_CONFIG_JSON解析。"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from app.config import Settings
from app.or_site_config import (
load_or_site_config_from_path,
merge_video_rtsp_urls_into_file,
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"}
def test_merge_preserves_bindings(tmp_path: Path) -> None:
p = tmp_path / "site.json"
p.write_text(
'{"video_rtsp_urls":{"old":"rtsp://old"},'
'"voice_or_room_bindings":[{"or_room_id":"R","camera_ids":["x"],'
'"voice_terminal_id":"T"}]}',
encoding="utf-8",
)
merge_video_rtsp_urls_into_file(
p, {"x": "rtsp://127.0.0.1:1/p"}, replace_host="127.0.0.1"
)
cfg = load_or_site_config_from_path(p)
assert cfg.video_rtsp_urls == {"x": "rtsp://127.0.0.1:1/p"}
assert cfg.voice_bindings.resolve_terminal(["x"]) == "T"