Outgoing WS now echoes, websock reclamation after unmount cleanup, hash fix for empty contacts, no double bot broadcast, AGENTS.md + test fixes (this should have been more than one commit lol)

This commit is contained in:
Jack Kingsman
2026-02-11 23:59:05 -08:00
parent fef18a943c
commit f73fa54532
14 changed files with 816 additions and 1391 deletions
+106
View File
@@ -92,6 +92,112 @@ class TestMessagesEndpoint:
assert response.status_code == 503
def test_send_direct_message_emits_websocket_message_event(self):
"""POST /messages/direct should emit a WS message event for other clients."""
from fastapi.testclient import TestClient
from meshcore import EventType
mock_mc = MagicMock()
mock_mc.get_contact_by_key_prefix.return_value = {"public_key": "ab" * 32}
mock_mc.commands.add_contact = AsyncMock(
return_value=MagicMock(type=EventType.OK, payload={})
)
mock_mc.commands.send_msg = AsyncMock(
return_value=MagicMock(type=EventType.MSG_SENT, payload={})
)
mock_contact = MagicMock()
mock_contact.public_key = "ab" * 32
mock_contact.to_radio_dict.return_value = {"public_key": "ab" * 32}
def _capture_task(coro):
coro.close()
return MagicMock()
with (
patch("app.dependencies.radio_manager") as mock_rm,
patch(
"app.repository.ContactRepository.get_by_key_or_prefix",
new=AsyncMock(return_value=mock_contact),
),
patch("app.repository.ContactRepository.update_last_contacted", new=AsyncMock()),
patch("app.repository.MessageRepository.create", new=AsyncMock(return_value=123)),
patch("app.bot.run_bot_for_message", new=AsyncMock()),
patch("app.routers.messages.asyncio.create_task", side_effect=_capture_task),
patch("app.routers.messages.broadcast_event", create=True) as mock_broadcast,
):
mock_rm.is_connected = True
mock_rm.meshcore = mock_mc
from app.main import app
client = TestClient(app)
response = client.post(
"/api/messages/direct",
json={"destination": mock_contact.public_key, "text": "Hello"},
)
assert response.status_code == 200
mock_broadcast.assert_called_once()
event_type, payload = mock_broadcast.call_args.args
assert event_type == "message"
assert payload["id"] == 123
assert payload["type"] == "PRIV"
def test_send_channel_message_emits_websocket_message_event(self):
"""POST /messages/channel should emit a WS message event for other clients."""
from fastapi.testclient import TestClient
from meshcore import EventType
mock_mc = MagicMock()
mock_mc.self_info = {"name": "TestNode"}
ok_result = MagicMock(type=EventType.MSG_SENT, payload={})
mock_mc.commands.set_channel = AsyncMock(return_value=ok_result)
mock_mc.commands.send_chan_msg = AsyncMock(return_value=ok_result)
mock_channel = MagicMock()
mock_channel.name = "Public"
mock_channel.key = "AA" * 16
def _capture_task(coro):
coro.close()
return MagicMock()
with (
patch("app.dependencies.radio_manager") as mock_rm,
patch(
"app.repository.ChannelRepository.get_by_key",
new=AsyncMock(return_value=mock_channel),
),
patch(
"app.repository.AppSettingsRepository.get",
new=AsyncMock(return_value=MagicMock(experimental_channel_double_send=False)),
),
patch("app.repository.MessageRepository.create", new=AsyncMock(return_value=456)),
patch("app.repository.MessageRepository.get_ack_count", new=AsyncMock(return_value=0)),
patch("app.decoder.calculate_channel_hash", return_value="abcd"),
patch("app.bot.run_bot_for_message", new=AsyncMock()),
patch("app.routers.messages.asyncio.create_task", side_effect=_capture_task),
patch("app.routers.messages.broadcast_event", create=True) as mock_broadcast,
):
mock_rm.is_connected = True
mock_rm.meshcore = mock_mc
from app.main import app
client = TestClient(app)
response = client.post(
"/api/messages/channel",
json={"channel_key": mock_channel.key, "text": "Hello room"},
)
assert response.status_code == 200
mock_broadcast.assert_called_once()
event_type, payload = mock_broadcast.call_args.args
assert event_type == "message"
assert payload["id"] == 456
assert payload["type"] == "CHAN"
def test_send_direct_message_contact_not_found(self):
"""Sending to unknown contact returns 404."""
from fastapi.testclient import TestClient
+40
View File
@@ -16,6 +16,7 @@ from app.event_handlers import (
register_event_handlers,
track_pending_ack,
)
from app.repository import AmbiguousPublicKeyPrefixError
@pytest.fixture(autouse=True)
@@ -306,6 +307,45 @@ class TestContactMessageCLIFiltering:
# SHOULD still be processed (defaults to txt_type=0)
mock_repo.create.assert_called_once()
@pytest.mark.asyncio
async def test_ambiguous_prefix_stores_dm_under_prefix(self):
"""Ambiguous sender prefixes should still be stored under the prefix key."""
from app.event_handlers import on_contact_message
with (
patch("app.event_handlers.MessageRepository") as mock_repo,
patch("app.event_handlers.ContactRepository") as mock_contact_repo,
patch("app.event_handlers.broadcast_event") as mock_broadcast,
patch("app.bot.run_bot_for_message", new_callable=AsyncMock),
):
mock_repo.create = AsyncMock(return_value=77)
mock_contact_repo.get_by_key_or_prefix = AsyncMock(
side_effect=AmbiguousPublicKeyPrefixError(
"abc123",
[
"abc1230000000000000000000000000000000000000000000000000000000000",
"abc123ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
],
)
)
class MockEvent:
payload = {
"pubkey_prefix": "abc123",
"text": "hello from ambiguous prefix",
"txt_type": 0,
"sender_timestamp": 1700000000,
}
await on_contact_message(MockEvent())
mock_repo.create.assert_called_once()
assert mock_repo.create.await_args.kwargs["conversation_key"] == "abc123"
mock_broadcast.assert_called_once()
_, payload = mock_broadcast.call_args.args
assert payload["conversation_key"] == "abc123"
class TestEventHandlerRegistration:
"""Test event handler registration and cleanup."""
+56
View File
@@ -4,6 +4,7 @@ These tests verify that connect() routes to the correct transport method
based on settings.connection_type, and that connection_info is set correctly.
"""
import asyncio
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@@ -168,3 +169,58 @@ class TestRadioManagerConnect:
old_mc.disconnect.assert_awaited_once()
assert rm.meshcore is new_mc
class TestConnectionMonitor:
"""Tests for the background connection monitor loop."""
@pytest.mark.asyncio
async def test_monitor_does_not_mark_connected_when_setup_fails(self):
"""A reconnect with failing post-connect setup should not broadcast healthy status."""
from app.radio import RadioManager
rm = RadioManager()
rm._connection_info = "Serial: /dev/ttyUSB0"
rm._last_connected = True
rm._meshcore = MagicMock()
rm._meshcore.is_connected = False
reconnect_calls = 0
async def _reconnect(*args, **kwargs):
nonlocal reconnect_calls
reconnect_calls += 1
if reconnect_calls == 1:
rm._meshcore = MagicMock()
rm._meshcore.is_connected = True
return True
return False
sleep_calls = 0
async def _sleep(_seconds: float):
nonlocal sleep_calls
sleep_calls += 1
if sleep_calls >= 3:
raise asyncio.CancelledError()
rm.reconnect = AsyncMock(side_effect=_reconnect)
rm.post_connect_setup = AsyncMock(side_effect=RuntimeError("setup failed"))
with (
patch("app.radio.asyncio.sleep", side_effect=_sleep),
patch("app.websocket.broadcast_health") as mock_broadcast_health,
):
await rm.start_connection_monitor()
try:
await rm._reconnect_task
finally:
await rm.stop_connection_monitor()
# Should report connection lost, but not report healthy until setup succeeds.
mock_broadcast_health.assert_any_call(False, "Serial: /dev/ttyUSB0")
healthy_calls = [
call for call in mock_broadcast_health.call_args_list if call.args[0] is True
]
assert healthy_calls == []
assert rm._last_connected is False