mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
Enable bot responses to ourselves take 2
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
|
||||
@@ -115,7 +116,7 @@ async def send_direct_message(request: SendDirectMessageRequest) -> Message:
|
||||
track_pending_ack(ack_code, message_id, suggested_timeout)
|
||||
logger.debug("Tracking ACK %s for message %d", ack_code, message_id)
|
||||
|
||||
return Message(
|
||||
message = Message(
|
||||
id=message_id,
|
||||
type="PRIV",
|
||||
conversation_key=db_contact.public_key,
|
||||
@@ -126,6 +127,25 @@ async def send_direct_message(request: SendDirectMessageRequest) -> Message:
|
||||
acked=0,
|
||||
)
|
||||
|
||||
# Trigger bots for outgoing DMs (runs in background, doesn't block response)
|
||||
from app.bot import run_bot_for_message
|
||||
|
||||
asyncio.create_task(
|
||||
run_bot_for_message(
|
||||
sender_name=None,
|
||||
sender_key=db_contact.public_key,
|
||||
message_text=request.text,
|
||||
is_dm=True,
|
||||
channel_key=None,
|
||||
channel_name=None,
|
||||
sender_timestamp=now,
|
||||
path=None,
|
||||
is_outgoing=True,
|
||||
)
|
||||
)
|
||||
|
||||
return message
|
||||
|
||||
|
||||
# Temporary radio slot used for sending channel messages
|
||||
TEMP_RADIO_SLOT = 0
|
||||
@@ -213,7 +233,7 @@ async def send_channel_message(request: SendChannelMessageRequest) -> Message:
|
||||
detail="Failed to store outgoing message - unexpected duplicate",
|
||||
)
|
||||
|
||||
return Message(
|
||||
message = Message(
|
||||
id=message_id,
|
||||
type="CHAN",
|
||||
conversation_key=channel_key_upper,
|
||||
@@ -223,3 +243,22 @@ async def send_channel_message(request: SendChannelMessageRequest) -> Message:
|
||||
outgoing=True,
|
||||
acked=0,
|
||||
)
|
||||
|
||||
# Trigger bots for outgoing channel messages (runs in background, doesn't block response)
|
||||
from app.bot import run_bot_for_message
|
||||
|
||||
asyncio.create_task(
|
||||
run_bot_for_message(
|
||||
sender_name=radio_name or None,
|
||||
sender_key=None,
|
||||
message_text=request.text,
|
||||
is_dm=False,
|
||||
channel_key=channel_key_upper,
|
||||
channel_name=db_channel.name,
|
||||
sender_timestamp=now,
|
||||
path=None,
|
||||
is_outgoing=True,
|
||||
)
|
||||
)
|
||||
|
||||
return message
|
||||
|
||||
196
tests/test_send_messages.py
Normal file
196
tests/test_send_messages.py
Normal file
@@ -0,0 +1,196 @@
|
||||
"""Tests for bot triggering on outgoing messages sent via the messages router."""
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from meshcore import EventType
|
||||
|
||||
from app.models import Channel, Contact, SendChannelMessageRequest, SendDirectMessageRequest
|
||||
from app.routers.messages import send_channel_message, send_direct_message
|
||||
|
||||
|
||||
def _make_radio_result(payload=None):
|
||||
"""Create a mock radio command result."""
|
||||
result = MagicMock()
|
||||
result.type = EventType.MSG_SENT
|
||||
result.payload = payload or {}
|
||||
return result
|
||||
|
||||
|
||||
def _make_mc(name="TestNode"):
|
||||
"""Create a mock MeshCore connection."""
|
||||
mc = MagicMock()
|
||||
mc.self_info = {"name": name}
|
||||
mc.commands = MagicMock()
|
||||
mc.commands.send_msg = AsyncMock(return_value=_make_radio_result())
|
||||
mc.commands.send_chan_msg = AsyncMock(return_value=_make_radio_result())
|
||||
mc.commands.add_contact = AsyncMock(return_value=_make_radio_result())
|
||||
mc.commands.set_channel = AsyncMock(return_value=_make_radio_result())
|
||||
mc.get_contact_by_key_prefix = MagicMock(return_value=None)
|
||||
return mc
|
||||
|
||||
|
||||
class TestOutgoingDMBotTrigger:
|
||||
"""Test that sending a DM triggers bots with is_outgoing=True."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_dm_triggers_bot(self):
|
||||
"""Sending a DM creates a background task to run bots."""
|
||||
mc = _make_mc()
|
||||
db_contact = Contact(public_key="ab" * 32, name="Alice")
|
||||
|
||||
with (
|
||||
patch("app.routers.messages.require_connected", return_value=mc),
|
||||
patch(
|
||||
"app.repository.ContactRepository.get_by_key_or_prefix",
|
||||
new=AsyncMock(return_value=db_contact),
|
||||
),
|
||||
patch("app.repository.ContactRepository.update_last_contacted", new=AsyncMock()),
|
||||
patch("app.repository.MessageRepository.create", new=AsyncMock(return_value=1)),
|
||||
patch("app.bot.run_bot_for_message", new=AsyncMock()) as mock_bot,
|
||||
):
|
||||
request = SendDirectMessageRequest(
|
||||
destination=db_contact.public_key, text="!lasttime Alice"
|
||||
)
|
||||
await send_direct_message(request)
|
||||
|
||||
# Let the background task run
|
||||
await asyncio.sleep(0)
|
||||
|
||||
mock_bot.assert_called_once()
|
||||
call_kwargs = mock_bot.call_args[1]
|
||||
assert call_kwargs["message_text"] == "!lasttime Alice"
|
||||
assert call_kwargs["is_dm"] is True
|
||||
assert call_kwargs["is_outgoing"] is True
|
||||
assert call_kwargs["sender_key"] == db_contact.public_key
|
||||
assert call_kwargs["channel_key"] is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_dm_bot_does_not_block_response(self):
|
||||
"""Bot trigger runs in background and doesn't delay the message response."""
|
||||
mc = _make_mc()
|
||||
db_contact = Contact(public_key="ab" * 32, name="Alice")
|
||||
|
||||
# Bot that would take a long time
|
||||
slow_bot = AsyncMock(side_effect=lambda **kw: asyncio.sleep(10))
|
||||
|
||||
with (
|
||||
patch("app.routers.messages.require_connected", return_value=mc),
|
||||
patch(
|
||||
"app.repository.ContactRepository.get_by_key_or_prefix",
|
||||
new=AsyncMock(return_value=db_contact),
|
||||
),
|
||||
patch("app.repository.ContactRepository.update_last_contacted", new=AsyncMock()),
|
||||
patch("app.repository.MessageRepository.create", new=AsyncMock(return_value=1)),
|
||||
patch("app.bot.run_bot_for_message", new=slow_bot),
|
||||
):
|
||||
request = SendDirectMessageRequest(destination=db_contact.public_key, text="Hello")
|
||||
# This should return immediately, not wait 10 seconds
|
||||
message = await send_direct_message(request)
|
||||
assert message.text == "Hello"
|
||||
assert message.outgoing is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_dm_passes_no_sender_name(self):
|
||||
"""Outgoing DMs pass sender_name=None (we are the sender)."""
|
||||
mc = _make_mc()
|
||||
db_contact = Contact(public_key="cd" * 32, name="Bob")
|
||||
|
||||
with (
|
||||
patch("app.routers.messages.require_connected", return_value=mc),
|
||||
patch(
|
||||
"app.repository.ContactRepository.get_by_key_or_prefix",
|
||||
new=AsyncMock(return_value=db_contact),
|
||||
),
|
||||
patch("app.repository.ContactRepository.update_last_contacted", new=AsyncMock()),
|
||||
patch("app.repository.MessageRepository.create", new=AsyncMock(return_value=1)),
|
||||
patch("app.bot.run_bot_for_message", new=AsyncMock()) as mock_bot,
|
||||
):
|
||||
request = SendDirectMessageRequest(destination=db_contact.public_key, text="test")
|
||||
await send_direct_message(request)
|
||||
await asyncio.sleep(0)
|
||||
|
||||
call_kwargs = mock_bot.call_args[1]
|
||||
assert call_kwargs["sender_name"] is None
|
||||
|
||||
|
||||
class TestOutgoingChannelBotTrigger:
|
||||
"""Test that sending a channel message triggers bots with is_outgoing=True."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_channel_msg_triggers_bot(self):
|
||||
"""Sending a channel message creates a background task to run bots."""
|
||||
mc = _make_mc(name="MyNode")
|
||||
db_channel = Channel(key="aa" * 16, name="#general")
|
||||
|
||||
with (
|
||||
patch("app.routers.messages.require_connected", return_value=mc),
|
||||
patch(
|
||||
"app.repository.ChannelRepository.get_by_key",
|
||||
new=AsyncMock(return_value=db_channel),
|
||||
),
|
||||
patch("app.repository.MessageRepository.create", new=AsyncMock(return_value=1)),
|
||||
patch("app.decoder.calculate_channel_hash", return_value="abcd"),
|
||||
patch("app.bot.run_bot_for_message", new=AsyncMock()) as mock_bot,
|
||||
):
|
||||
request = SendChannelMessageRequest(
|
||||
channel_key=db_channel.key, text="!lasttime5 someone"
|
||||
)
|
||||
await send_channel_message(request)
|
||||
await asyncio.sleep(0)
|
||||
|
||||
mock_bot.assert_called_once()
|
||||
call_kwargs = mock_bot.call_args[1]
|
||||
assert call_kwargs["message_text"] == "!lasttime5 someone"
|
||||
assert call_kwargs["is_dm"] is False
|
||||
assert call_kwargs["is_outgoing"] is True
|
||||
assert call_kwargs["channel_key"] == db_channel.key.upper()
|
||||
assert call_kwargs["channel_name"] == "#general"
|
||||
assert call_kwargs["sender_name"] == "MyNode"
|
||||
assert call_kwargs["sender_key"] is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_channel_msg_no_radio_name(self):
|
||||
"""When radio has no name, sender_name is None."""
|
||||
mc = _make_mc(name="")
|
||||
db_channel = Channel(key="bb" * 16, name="#test")
|
||||
|
||||
with (
|
||||
patch("app.routers.messages.require_connected", return_value=mc),
|
||||
patch(
|
||||
"app.repository.ChannelRepository.get_by_key",
|
||||
new=AsyncMock(return_value=db_channel),
|
||||
),
|
||||
patch("app.repository.MessageRepository.create", new=AsyncMock(return_value=1)),
|
||||
patch("app.decoder.calculate_channel_hash", return_value="abcd"),
|
||||
patch("app.bot.run_bot_for_message", new=AsyncMock()) as mock_bot,
|
||||
):
|
||||
request = SendChannelMessageRequest(channel_key=db_channel.key, text="hello")
|
||||
await send_channel_message(request)
|
||||
await asyncio.sleep(0)
|
||||
|
||||
call_kwargs = mock_bot.call_args[1]
|
||||
assert call_kwargs["sender_name"] is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_channel_msg_bot_does_not_block_response(self):
|
||||
"""Bot trigger runs in background and doesn't delay the message response."""
|
||||
mc = _make_mc(name="MyNode")
|
||||
db_channel = Channel(key="cc" * 16, name="#slow")
|
||||
|
||||
slow_bot = AsyncMock(side_effect=lambda **kw: asyncio.sleep(10))
|
||||
|
||||
with (
|
||||
patch("app.routers.messages.require_connected", return_value=mc),
|
||||
patch(
|
||||
"app.repository.ChannelRepository.get_by_key",
|
||||
new=AsyncMock(return_value=db_channel),
|
||||
),
|
||||
patch("app.repository.MessageRepository.create", new=AsyncMock(return_value=1)),
|
||||
patch("app.decoder.calculate_channel_hash", return_value="abcd"),
|
||||
patch("app.bot.run_bot_for_message", new=slow_bot),
|
||||
):
|
||||
request = SendChannelMessageRequest(channel_key=db_channel.key, text="test")
|
||||
message = await send_channel_message(request)
|
||||
assert message.outgoing is True
|
||||
Reference in New Issue
Block a user