feat(tests): add backend test coverage with new repository server manager tests and additional web audio bridge scenarios

This commit is contained in:
Ivan
2026-04-23 02:28:52 -05:00
parent 54de21e265
commit 39a331450a
3 changed files with 541 additions and 2 deletions
+15 -1
View File
@@ -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()
+316 -1
View File
@@ -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