mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-08-28 00:24:03 +00:00
feat(tests): add backend test coverage with new repository server manager tests and additional web audio bridge scenarios
This commit is contained in:
@@ -8,7 +8,7 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from meshchatx.src.backend.map_manager import MapManager
|
||||
from meshchatx.src.backend.map_manager import MAX_EXPORT_TILES, MapManager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -108,3 +108,17 @@ def test_start_export_status(mock_config, temp_dir):
|
||||
assert export_id == "test_id"
|
||||
status = mm.get_export_status(export_id)
|
||||
assert status["status"] == "starting"
|
||||
|
||||
|
||||
def test_count_export_tiles_world_low_zoom(mock_config, temp_dir):
|
||||
mm = MapManager(mock_config, temp_dir)
|
||||
bbox = [-180, -85.051129, 180, 85.051129]
|
||||
n = mm.count_export_tiles(bbox, 0, 4)
|
||||
assert n > 0
|
||||
assert n < MAX_EXPORT_TILES
|
||||
|
||||
|
||||
def test_count_export_tiles_dedupes(mock_config, temp_dir):
|
||||
mm = MapManager(mock_config, temp_dir)
|
||||
single = mm.count_export_tiles([0, 0, 0.0001, 0.0001], 2, 2)
|
||||
assert single > 0
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
import time
|
||||
import urllib.request
|
||||
from subprocess import CompletedProcess
|
||||
from unittest.mock import patch
|
||||
|
||||
from meshchatx.src.backend.repository_server_manager import (
|
||||
RepositoryServerManager,
|
||||
build_repository_index_html,
|
||||
bundled_pip_targets,
|
||||
download_bundled_wheels_to_directory,
|
||||
meshchat_bundle_project_root,
|
||||
)
|
||||
|
||||
|
||||
def test_bundled_pip_targets_includes_extra_from_env(monkeypatch):
|
||||
monkeypatch.setenv("MESHCHAT_REPOSITORY_EXTRA_PIP", "foo, bar")
|
||||
names = bundled_pip_targets()
|
||||
assert "foo" in names
|
||||
assert "bar" in names
|
||||
assert "rns" in names
|
||||
assert "rnspure" in names
|
||||
|
||||
|
||||
def test_seed_copies_bundled_wheels_from_public(tmp_path):
|
||||
identity = tmp_path / "identity"
|
||||
identity.mkdir()
|
||||
public = tmp_path / "public"
|
||||
bundled_pub = public / "repository-server-bundled" / "bundled"
|
||||
bundled_pub.mkdir(parents=True)
|
||||
(bundled_pub / "from_build.whl").write_bytes(b"wheeldata")
|
||||
mgr = RepositoryServerManager(str(identity), public_dir=str(public))
|
||||
rows = mgr.list_entries()
|
||||
bundled_names = [r["name"] for r in rows if r["source"] == "bundled"]
|
||||
assert "from_build.whl" in bundled_names
|
||||
|
||||
|
||||
def test_seed_skips_when_wheel_already_present(tmp_path):
|
||||
identity = tmp_path / "identity"
|
||||
identity.mkdir()
|
||||
public = tmp_path / "public"
|
||||
bundled_pub = public / "repository-server-bundled" / "bundled"
|
||||
bundled_pub.mkdir(parents=True)
|
||||
(bundled_pub / "same.whl").write_bytes(b"from_public")
|
||||
dest = identity / "repository-server" / "bundled"
|
||||
dest.mkdir(parents=True)
|
||||
(dest / "same.whl").write_bytes(b"user_keeps")
|
||||
RepositoryServerManager(str(identity), public_dir=str(public))
|
||||
assert (dest / "same.whl").read_bytes() == b"user_keeps"
|
||||
|
||||
|
||||
def test_meshchat_bundle_project_root_exists():
|
||||
root = meshchat_bundle_project_root()
|
||||
assert root is not None
|
||||
assert (root / "pyproject.toml").is_file()
|
||||
assert (root / "meshchatx").is_dir()
|
||||
|
||||
|
||||
def test_build_repository_index_html_lists_files(tmp_path):
|
||||
bundled = tmp_path / "bundled"
|
||||
uploads = tmp_path / "uploads"
|
||||
bundled.mkdir()
|
||||
uploads.mkdir()
|
||||
(bundled / "pkg.whl").write_bytes(b"x")
|
||||
(uploads / "note.txt").write_bytes(b"ab")
|
||||
html_out = build_repository_index_html(str(bundled), str(uploads))
|
||||
assert "MeshChatX repository" in html_out
|
||||
assert "pkg.whl" in html_out
|
||||
assert "note.txt" in html_out
|
||||
assert 'href="bundled/' in html_out
|
||||
assert 'href="uploads/' in html_out
|
||||
|
||||
|
||||
def test_save_list_delete_upload(tmp_path):
|
||||
mgr = RepositoryServerManager(str(tmp_path))
|
||||
ok, err = mgr.save_upload("test.whl", b"abc")
|
||||
assert ok and err is None
|
||||
rows = mgr.list_entries()
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["name"] == "test.whl"
|
||||
assert rows[0]["source"] == "upload"
|
||||
ok2, err2 = mgr.delete_upload("test.whl")
|
||||
assert ok2 and err2 is None
|
||||
assert mgr.list_entries() == []
|
||||
|
||||
|
||||
def test_save_rejects_bad_filename(tmp_path):
|
||||
mgr = RepositoryServerManager(str(tmp_path))
|
||||
ok, err = mgr.save_upload("../evil.whl", b"x")
|
||||
assert not ok
|
||||
|
||||
|
||||
@patch(
|
||||
"meshchatx.src.backend.repository_server_manager.download_bundled_wheels_to_directory"
|
||||
)
|
||||
def test_refresh_invokes_bundled_downloader(mock_dl, tmp_path):
|
||||
mock_dl.return_value = {"ok": True, "downloaded": ["rns"], "failed": {}}
|
||||
mgr = RepositoryServerManager(str(tmp_path))
|
||||
out = mgr.refresh_bundled_wheels()
|
||||
assert out["ok"] is True
|
||||
mock_dl.assert_called_once()
|
||||
assert mock_dl.call_args.kwargs.get("pip") is None
|
||||
|
||||
|
||||
@patch("meshchatx.src.backend.repository_server_manager._download_wheel_via_pypi_index")
|
||||
@patch("meshchatx.src.backend.repository_server_manager.subprocess.run")
|
||||
@patch("meshchatx.src.backend.repository_server_manager.shutil.which")
|
||||
def test_download_bundled_wheels_to_directory(
|
||||
mock_which, mock_run, mock_pypi, tmp_path, monkeypatch
|
||||
):
|
||||
monkeypatch.setenv("MESHCHAT_REPOSITORY_EXTRA_PIP", "")
|
||||
mock_which.return_value = "/fake/pip3"
|
||||
mock_pypi.return_value = (False, "offline")
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
assert cmd[0] == "/fake/pip3"
|
||||
assert cmd[1] in ("download", "wheel")
|
||||
assert cmd[2] == "--no-deps"
|
||||
assert cmd[3] in ("-d", "-w")
|
||||
return CompletedProcess(cmd, 0, "", "")
|
||||
|
||||
mock_run.side_effect = fake_run
|
||||
dest = tmp_path / "out"
|
||||
out = download_bundled_wheels_to_directory(dest, "/fake/pip3")
|
||||
assert out["ok"] is True
|
||||
n = len(bundled_pip_targets())
|
||||
assert mock_run.call_count == n
|
||||
assert mock_pypi.call_count == n - 1
|
||||
|
||||
|
||||
@patch("meshchatx.src.backend.repository_server_manager._download_wheel_via_pypi_index")
|
||||
@patch("meshchatx.src.backend.repository_server_manager.subprocess.run")
|
||||
@patch("meshchatx.src.backend.repository_server_manager.shutil.which")
|
||||
def test_refresh_calls_pip_after_pypi_fails(
|
||||
mock_which, mock_run, mock_pypi, tmp_path, monkeypatch
|
||||
):
|
||||
monkeypatch.setenv("MESHCHAT_REPOSITORY_EXTRA_PIP", "")
|
||||
mock_which.return_value = "/fake/pip3"
|
||||
mock_pypi.return_value = (False, "offline")
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
assert cmd[0] == "/fake/pip3"
|
||||
assert cmd[1] in ("download", "wheel")
|
||||
assert cmd[2] == "--no-deps"
|
||||
assert cmd[3] in ("-d", "-w")
|
||||
return CompletedProcess(cmd, 0, "", "")
|
||||
|
||||
mock_run.side_effect = fake_run
|
||||
mgr = RepositoryServerManager(str(tmp_path))
|
||||
out = mgr.refresh_bundled_wheels()
|
||||
assert out["ok"] is True
|
||||
n = len(bundled_pip_targets())
|
||||
assert mock_run.call_count == n
|
||||
assert mock_pypi.call_count == n - 1
|
||||
|
||||
|
||||
def test_http_start_stop_and_status(tmp_path):
|
||||
mgr = RepositoryServerManager(str(tmp_path))
|
||||
assert mgr.status()["http"]["running"] is False
|
||||
first = mgr.start_http_server("127.0.0.1", 0)
|
||||
assert first["ok"] is True
|
||||
assert first["port"] > 0
|
||||
time.sleep(0.05)
|
||||
st = mgr.status()["http"]
|
||||
assert st["running"] is True
|
||||
assert st["url"] == f"http://127.0.0.1:{first['port']}/"
|
||||
assert st["last_port"] == first["port"]
|
||||
second = mgr.start_http_server("127.0.0.1", first["port"])
|
||||
assert second["ok"] is False
|
||||
assert second["error"] == "already_running"
|
||||
mgr.stop_http_server()
|
||||
assert mgr.status()["http"]["running"] is False
|
||||
|
||||
|
||||
def test_http_invalid_host(tmp_path):
|
||||
mgr = RepositoryServerManager(str(tmp_path))
|
||||
out = mgr.start_http_server(" ", 8787)
|
||||
assert out["ok"] is False
|
||||
assert out["error"] == "invalid_host"
|
||||
|
||||
|
||||
def test_http_get_root(tmp_path):
|
||||
mgr = RepositoryServerManager(str(tmp_path))
|
||||
mgr.save_upload("listed.whl", b"wheel")
|
||||
out = mgr.start_http_server("127.0.0.1", 0)
|
||||
assert out["ok"] is True
|
||||
time.sleep(0.08)
|
||||
url = out["url"]
|
||||
try:
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||
assert resp.status == 200
|
||||
body = resp.read().decode("utf-8")
|
||||
assert "listed.whl" in body
|
||||
assert "uploads/" in body
|
||||
finally:
|
||||
mgr.stop_http_server()
|
||||
|
||||
|
||||
def test_http_restart_uses_last_listen(tmp_path):
|
||||
mgr = RepositoryServerManager(str(tmp_path))
|
||||
a = mgr.start_http_server("127.0.0.1", 0)
|
||||
assert a["ok"] is True
|
||||
port = a["port"]
|
||||
mgr.stop_http_server()
|
||||
b = mgr.restart_http_server(None, None)
|
||||
assert b["ok"] is True
|
||||
assert b["port"] == port
|
||||
mgr.stop_http_server()
|
||||
@@ -1,6 +1,7 @@
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import numpy as np
|
||||
@@ -32,6 +33,28 @@ def test_web_audio_source_pushes_frames():
|
||||
assert len(sink.frames) == 1
|
||||
|
||||
|
||||
def test_web_audio_source_empty_pcm_does_not_push():
|
||||
sink = _DummySink()
|
||||
src = WebAudioSource(target_frame_ms=60, sink=sink)
|
||||
src.push_pcm(b"")
|
||||
assert len(sink.frames) == 0
|
||||
|
||||
|
||||
def test_web_audio_source_respects_sink_can_receive_false():
|
||||
sink = MagicMock()
|
||||
sink.can_receive.return_value = False
|
||||
src = WebAudioSource(target_frame_ms=60, sink=sink)
|
||||
src.push_pcm(np.zeros(8, dtype=np.int16).tobytes())
|
||||
sink.handle_frame.assert_not_called()
|
||||
|
||||
|
||||
def test_web_audio_source_can_receive_and_handle_frame_are_safe():
|
||||
sink = _DummySink()
|
||||
src = WebAudioSource(target_frame_ms=0, sink=sink)
|
||||
assert src.can_receive() is True
|
||||
src.handle_frame(None, None)
|
||||
|
||||
|
||||
def test_web_audio_sink_encodes_and_sends_bytes():
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
@@ -47,6 +70,36 @@ def test_web_audio_sink_encodes_and_sends_bytes():
|
||||
assert sent, "expected audio bytes to be queued for sending"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_web_audio_sink_forwards_non_numpy_frame_unchanged():
|
||||
sent = []
|
||||
|
||||
async def _send_bytes(data):
|
||||
sent.append(data)
|
||||
|
||||
sink = WebAudioSink(asyncio.get_running_loop(), _send_bytes)
|
||||
sink.handle_frame(b"\xff\x00", MagicMock())
|
||||
await asyncio.sleep(0.02)
|
||||
assert sent == [b"\xff\x00"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_web_audio_sink_numpy_frame_clips_to_int16_range():
|
||||
sent = []
|
||||
|
||||
async def _send_bytes(data):
|
||||
sent.append(data)
|
||||
|
||||
sink = WebAudioSink(asyncio.get_running_loop(), _send_bytes)
|
||||
arr = np.array([[2.0], [-2.0]], dtype=np.float32)
|
||||
sink.handle_frame(arr, None)
|
||||
await asyncio.sleep(0.02)
|
||||
assert len(sent) == 1
|
||||
out = np.frombuffer(sent[0], dtype=np.int16)
|
||||
assert out[0] == 32767
|
||||
assert out[1] == -32767
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_web_audio_bridge_lazy_loop():
|
||||
"""Test that WebAudioBridge retrieves the loop lazily to avoid startup crashes."""
|
||||
@@ -78,6 +131,14 @@ def test_web_audio_config_disabled_without_config_manager():
|
||||
assert not bridge.config_enabled()
|
||||
|
||||
|
||||
def test_web_audio_config_enabled_false_without_flag_attribute():
|
||||
class _Cfg:
|
||||
pass
|
||||
|
||||
bridge = WebAudioBridge(None, _Cfg())
|
||||
assert bridge.config_enabled() is False
|
||||
|
||||
|
||||
def test_web_audio_allow_fallback_follows_config():
|
||||
cfg = MagicMock()
|
||||
cfg.telephone_web_audio_allow_fallback.get.return_value = True
|
||||
@@ -87,6 +148,14 @@ def test_web_audio_allow_fallback_follows_config():
|
||||
assert bridge.allow_fallback() is False
|
||||
|
||||
|
||||
def test_web_audio_allow_fallback_false_without_flag_attribute():
|
||||
class _Cfg:
|
||||
pass
|
||||
|
||||
bridge = WebAudioBridge(None, _Cfg())
|
||||
assert bridge.allow_fallback() is False
|
||||
|
||||
|
||||
def test_web_audio_bridge_asyncutils_fallback():
|
||||
"""Test that WebAudioBridge falls back to AsyncUtils.main_loop if no loop is running."""
|
||||
from meshchatx.src.backend.async_utils import AsyncUtils
|
||||
@@ -113,9 +182,255 @@ def test_attach_client_returns_false_without_active_call():
|
||||
tele_mgr.telephone.active_call = None
|
||||
bridge = WebAudioBridge(tele_mgr, MagicMock())
|
||||
|
||||
attached = bridge.attach_client(MagicMock())
|
||||
mock_client = MagicMock()
|
||||
attached = bridge.attach_client(mock_client)
|
||||
|
||||
assert attached is False
|
||||
assert mock_client not in bridge.clients
|
||||
|
||||
|
||||
class _TeleMgrNoTelephone:
|
||||
"""Telephone manager shape without a ``telephone`` attribute (edge case)."""
|
||||
|
||||
|
||||
def test_tele_returns_none_when_manager_has_no_telephone():
|
||||
bridge = WebAudioBridge(_TeleMgrNoTelephone(), MagicMock())
|
||||
assert bridge._tele() is None
|
||||
|
||||
|
||||
def test_push_client_frame_no_op_without_tx_source():
|
||||
bridge = WebAudioBridge(MagicMock(), MagicMock())
|
||||
bridge.tx_source = None
|
||||
bridge.push_client_frame(b"ignored")
|
||||
|
||||
|
||||
def test_push_client_frame_forwards_to_tx_source():
|
||||
bridge = WebAudioBridge(MagicMock(), MagicMock())
|
||||
mock_tx = MagicMock()
|
||||
bridge.tx_source = mock_tx
|
||||
bridge.push_client_frame(b"\x01\x02")
|
||||
mock_tx.push_pcm.assert_called_once_with(b"\x01\x02")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_status_uses_tele_frame_ms():
|
||||
tele = MagicMock()
|
||||
tele.target_frame_time_ms = 52
|
||||
tele_mgr = MagicMock()
|
||||
tele_mgr.telephone = tele
|
||||
bridge = WebAudioBridge(tele_mgr, MagicMock())
|
||||
client = AsyncMock()
|
||||
await bridge.send_status(client)
|
||||
payload = json.loads(client.send_str.await_args.args[0])
|
||||
assert payload == {"type": "web_audio.ready", "frame_ms": 52}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_status_defaults_frame_ms_when_missing():
|
||||
tele = MagicMock(spec=["active_call"])
|
||||
tele_mgr = MagicMock()
|
||||
tele_mgr.telephone = tele
|
||||
bridge = WebAudioBridge(tele_mgr, MagicMock())
|
||||
client = AsyncMock()
|
||||
await bridge.send_status(client)
|
||||
payload = json.loads(client.send_str.await_args.args[0])
|
||||
assert payload["type"] == "web_audio.ready"
|
||||
assert payload["frame_ms"] == 60
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_status_defaults_when_no_telephone():
|
||||
bridge = WebAudioBridge(_TeleMgrNoTelephone(), MagicMock())
|
||||
client = AsyncMock()
|
||||
await bridge.send_status(client)
|
||||
payload = json.loads(client.send_str.await_args.args[0])
|
||||
assert payload == {"type": "web_audio.ready", "frame_ms": 60}
|
||||
|
||||
|
||||
@patch("meshchatx.src.backend.web_audio_bridge.Pipeline")
|
||||
def test_attach_client_success_wires_telephony_and_dedupes_client(mock_pipeline_cls):
|
||||
"""LXST ``Pipeline`` validates sources; mock it so we only assert bridge wiring."""
|
||||
mock_receive_pipeline = MagicMock()
|
||||
mock_pipeline_cls.return_value = mock_receive_pipeline
|
||||
tele = MagicMock()
|
||||
tele.active_call = object()
|
||||
tele.target_frame_time_ms = 48
|
||||
tele.audio_input = MagicMock()
|
||||
tele.transmit_mixer = MagicMock()
|
||||
tele.transmit_mixer.should_run = False
|
||||
tele.audio_output = MagicMock()
|
||||
tele.receive_mixer = MagicMock()
|
||||
tele.receive_pipeline = None
|
||||
tele_mgr = MagicMock()
|
||||
tele_mgr.telephone = tele
|
||||
bridge = WebAudioBridge(tele_mgr, MagicMock())
|
||||
client = MagicMock()
|
||||
|
||||
assert bridge.attach_client(client) is True
|
||||
assert client in bridge.clients
|
||||
assert isinstance(bridge.tx_source, WebAudioSource)
|
||||
assert tele.audio_input is bridge.tx_source
|
||||
tele.transmit_mixer.start.assert_called_once()
|
||||
assert bridge.rx_tee is not None
|
||||
assert tele.audio_output is bridge.rx_tee
|
||||
assert tele.receive_pipeline is mock_receive_pipeline
|
||||
mock_receive_pipeline.start.assert_called_once()
|
||||
mock_pipeline_cls.assert_called_once()
|
||||
call_kw = mock_pipeline_cls.call_args.kwargs
|
||||
assert call_kw["source"] is tele.receive_mixer
|
||||
assert call_kw["sink"] is bridge.rx_tee
|
||||
|
||||
assert bridge.attach_client(client) is True
|
||||
assert len(bridge.clients) == 1
|
||||
mock_pipeline_cls.assert_called_once()
|
||||
|
||||
|
||||
@patch("meshchatx.src.backend.web_audio_bridge.Pipeline")
|
||||
def test_attach_rx_tee_includes_base_sink_when_audio_output_exists(mock_pipeline_cls):
|
||||
mock_pipeline_cls.return_value = MagicMock()
|
||||
base_out = MagicMock()
|
||||
tele = MagicMock()
|
||||
tele.active_call = object()
|
||||
tele.target_frame_time_ms = 60
|
||||
tele.audio_input = None
|
||||
tele.transmit_mixer = MagicMock()
|
||||
tele.transmit_mixer.should_run = True
|
||||
tele.audio_output = base_out
|
||||
tele.receive_mixer = MagicMock()
|
||||
tele.receive_pipeline = None
|
||||
tele_mgr = MagicMock()
|
||||
tele_mgr.telephone = tele
|
||||
bridge = WebAudioBridge(tele_mgr, MagicMock())
|
||||
assert bridge.attach_client(MagicMock()) is True
|
||||
assert len(bridge.rx_tee.sinks) == 2
|
||||
assert bridge.rx_tee.sinks[0] is base_out
|
||||
assert bridge.rx_tee.sinks[1] is bridge.rx_sink
|
||||
|
||||
|
||||
@patch("meshchatx.src.backend.web_audio_bridge.Pipeline")
|
||||
def test_attach_rx_tee_single_sink_when_no_base_audio_output(mock_pipeline_cls):
|
||||
mock_pipeline_cls.return_value = MagicMock()
|
||||
tele = MagicMock()
|
||||
tele.active_call = object()
|
||||
tele.target_frame_time_ms = 60
|
||||
tele.audio_input = None
|
||||
tele.transmit_mixer = MagicMock()
|
||||
tele.transmit_mixer.should_run = True
|
||||
tele.audio_output = None
|
||||
tele.receive_mixer = MagicMock()
|
||||
tele.receive_pipeline = None
|
||||
tele_mgr = MagicMock()
|
||||
tele_mgr.telephone = tele
|
||||
bridge = WebAudioBridge(tele_mgr, MagicMock())
|
||||
assert bridge.attach_client(MagicMock()) is True
|
||||
assert len(bridge.rx_tee.sinks) == 1
|
||||
assert bridge.rx_tee.sinks[0] is bridge.rx_sink
|
||||
|
||||
|
||||
def test_attach_client_returns_false_when_telephone_is_none():
|
||||
tele_mgr = MagicMock()
|
||||
tele_mgr.telephone = None
|
||||
bridge = WebAudioBridge(tele_mgr, MagicMock())
|
||||
c = MagicMock()
|
||||
assert bridge.attach_client(c) is False
|
||||
assert c not in bridge.clients
|
||||
|
||||
|
||||
def test_detach_client_ignores_unknown_client():
|
||||
bridge = WebAudioBridge(MagicMock(), MagicMock())
|
||||
bridge.clients.add(MagicMock())
|
||||
with patch.object(bridge, "_restore_host_audio") as restore:
|
||||
bridge.detach_client(MagicMock())
|
||||
restore.assert_not_called()
|
||||
|
||||
|
||||
def test_detach_non_last_client_does_not_restore_host_audio():
|
||||
cfg = MagicMock()
|
||||
cfg.telephone_web_audio_allow_fallback.get.return_value = True
|
||||
bridge = WebAudioBridge(MagicMock(), cfg)
|
||||
c1, c2 = MagicMock(), MagicMock()
|
||||
bridge.clients.update({c1, c2})
|
||||
with patch.object(bridge, "_restore_host_audio") as restore:
|
||||
bridge.detach_client(c1)
|
||||
restore.assert_not_called()
|
||||
assert c2 in bridge.clients
|
||||
|
||||
|
||||
def test_detach_last_client_restores_when_allow_fallback_enabled():
|
||||
cfg = MagicMock()
|
||||
cfg.telephone_web_audio_allow_fallback.get.return_value = True
|
||||
bridge = WebAudioBridge(MagicMock(), cfg)
|
||||
c = MagicMock()
|
||||
bridge.clients.add(c)
|
||||
with patch.object(bridge, "_restore_host_audio") as restore:
|
||||
bridge.detach_client(c)
|
||||
restore.assert_called_once()
|
||||
assert len(bridge.clients) == 0
|
||||
|
||||
|
||||
def test_detach_last_client_skips_restore_when_allow_fallback_disabled():
|
||||
cfg = MagicMock()
|
||||
cfg.telephone_web_audio_allow_fallback.get.return_value = False
|
||||
bridge = WebAudioBridge(MagicMock(), cfg)
|
||||
c = MagicMock()
|
||||
bridge.clients.add(c)
|
||||
with patch.object(bridge, "_restore_host_audio") as restore:
|
||||
bridge.detach_client(c)
|
||||
restore.assert_not_called()
|
||||
|
||||
|
||||
def test_on_call_ended_clears_clients_and_pipeline_handles():
|
||||
bridge = WebAudioBridge(MagicMock(), MagicMock())
|
||||
bridge.clients.update({MagicMock(), MagicMock()})
|
||||
bridge.tx_source = MagicMock()
|
||||
bridge.rx_sink = MagicMock()
|
||||
bridge.rx_tee = MagicMock()
|
||||
bridge.on_call_ended()
|
||||
assert len(bridge.clients) == 0
|
||||
assert bridge.tx_source is None
|
||||
assert bridge.rx_sink is None
|
||||
assert bridge.rx_tee is None
|
||||
|
||||
|
||||
def test_restore_host_audio_no_telephone_is_safe():
|
||||
bridge = WebAudioBridge(MagicMock(), MagicMock())
|
||||
bridge.telephone_manager = MagicMock()
|
||||
bridge.telephone_manager.telephone = None
|
||||
bridge.rx_tee = MagicMock()
|
||||
bridge._restore_host_audio()
|
||||
|
||||
|
||||
@patch("meshchatx.src.backend.web_audio_bridge.RNS.log")
|
||||
def test_ensure_remote_tx_swallows_source_init_failure(mock_log):
|
||||
tele = MagicMock()
|
||||
tele.transmit_mixer = MagicMock()
|
||||
tele.transmit_mixer.should_run = True
|
||||
bridge = WebAudioBridge(MagicMock(), MagicMock())
|
||||
bridge.tx_source = None
|
||||
with patch(
|
||||
"meshchatx.src.backend.web_audio_bridge.WebAudioSource",
|
||||
side_effect=RuntimeError("init failed"),
|
||||
):
|
||||
bridge._ensure_remote_tx(tele)
|
||||
assert bridge.tx_source is None
|
||||
mock_log.assert_called()
|
||||
|
||||
|
||||
@patch("meshchatx.src.backend.web_audio_bridge.RNS.log")
|
||||
def test_ensure_rx_tee_swallows_web_audio_sink_init_failure(mock_log):
|
||||
tele = MagicMock()
|
||||
tele.audio_output = None
|
||||
tele.receive_mixer = MagicMock()
|
||||
tele.receive_pipeline = None
|
||||
bridge = WebAudioBridge(MagicMock(), MagicMock())
|
||||
bridge.rx_sink = None
|
||||
with patch(
|
||||
"meshchatx.src.backend.web_audio_bridge.WebAudioSink",
|
||||
side_effect=RuntimeError("sink init failed"),
|
||||
):
|
||||
bridge._ensure_rx_tee(tele)
|
||||
assert bridge.rx_sink is None
|
||||
mock_log.assert_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user