mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-06 08:43:36 +02:00
Add experimental double send
This commit is contained in:
@@ -133,6 +133,7 @@ export interface BotConfig {
|
||||
|
||||
export interface AppSettings {
|
||||
max_radio_contacts: number;
|
||||
experimental_channel_double_send: boolean;
|
||||
favorites: { type: string; id: string }[];
|
||||
auto_decrypt_dm_on_advert: boolean;
|
||||
sidebar_sort_order: string;
|
||||
|
||||
+2
-1
@@ -169,7 +169,7 @@ class TestMessagesEndpoint:
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_channel_message_duplicate_returns_500(self):
|
||||
"""If MessageRepository.create returns None (duplicate), returns 500."""
|
||||
from app.models import SendChannelMessageRequest
|
||||
from app.models import AppSettings, SendChannelMessageRequest
|
||||
from app.routers.messages import send_channel_message
|
||||
|
||||
mock_mc = MagicMock()
|
||||
@@ -187,6 +187,7 @@ class TestMessagesEndpoint:
|
||||
with (
|
||||
patch("app.dependencies.radio_manager") as mock_rm,
|
||||
patch("app.repository.ChannelRepository") as mock_chan_repo,
|
||||
patch("app.repository.AppSettingsRepository.get", new=AsyncMock(return_value=AppSettings())),
|
||||
patch("app.routers.messages.MessageRepository") as mock_msg_repo,
|
||||
):
|
||||
mock_rm.is_connected = True
|
||||
|
||||
+10
-10
@@ -100,8 +100,8 @@ class TestMigration001:
|
||||
# Run migrations
|
||||
applied = await run_migrations(conn)
|
||||
|
||||
assert applied == 15 # All 15 migrations run
|
||||
assert await get_version(conn) == 15
|
||||
assert applied == 16 # All 16 migrations run
|
||||
assert await get_version(conn) == 16
|
||||
|
||||
# Verify columns exist by inserting and selecting
|
||||
await conn.execute(
|
||||
@@ -183,9 +183,9 @@ class TestMigration001:
|
||||
applied1 = await run_migrations(conn)
|
||||
applied2 = await run_migrations(conn)
|
||||
|
||||
assert applied1 == 15 # All 15 migrations run
|
||||
assert applied1 == 16 # All 16 migrations run
|
||||
assert applied2 == 0 # No migrations on second run
|
||||
assert await get_version(conn) == 15
|
||||
assert await get_version(conn) == 16
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
@@ -245,9 +245,9 @@ class TestMigration001:
|
||||
# Run migrations - should not fail
|
||||
applied = await run_migrations(conn)
|
||||
|
||||
# All 15 migrations applied (version incremented) but no error
|
||||
assert applied == 15
|
||||
assert await get_version(conn) == 15
|
||||
# All 16 migrations applied (version incremented) but no error
|
||||
assert applied == 16
|
||||
assert await get_version(conn) == 16
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
@@ -374,10 +374,10 @@ class TestMigration013:
|
||||
)
|
||||
await conn.commit()
|
||||
|
||||
# Run migration 13 (plus 14+15 which also run)
|
||||
# Run migration 13 (plus 14+15+16 which also run)
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 3
|
||||
assert await get_version(conn) == 15
|
||||
assert applied == 4
|
||||
assert await get_version(conn) == 16
|
||||
|
||||
# Verify bots array was created with migrated data
|
||||
cursor = await conn.execute("SELECT bots FROM app_settings WHERE id = 1")
|
||||
|
||||
@@ -428,6 +428,7 @@ class TestAppSettingsRepository:
|
||||
mock_cursor.fetchone = AsyncMock(
|
||||
return_value={
|
||||
"max_radio_contacts": 250,
|
||||
"experimental_channel_double_send": 1,
|
||||
"favorites": "{not-json",
|
||||
"auto_decrypt_dm_on_advert": 1,
|
||||
"sidebar_sort_order": "invalid",
|
||||
@@ -448,6 +449,7 @@ class TestAppSettingsRepository:
|
||||
settings = await AppSettingsRepository.get()
|
||||
|
||||
assert settings.max_radio_contacts == 250
|
||||
assert settings.experimental_channel_double_send is True
|
||||
assert settings.favorites == []
|
||||
assert settings.last_message_times == {}
|
||||
assert settings.sidebar_sort_order == "recent"
|
||||
|
||||
+106
-1
@@ -6,7 +6,13 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import pytest
|
||||
from meshcore import EventType
|
||||
|
||||
from app.models import Channel, Contact, SendChannelMessageRequest, SendDirectMessageRequest
|
||||
from app.models import (
|
||||
AppSettings,
|
||||
Channel,
|
||||
Contact,
|
||||
SendChannelMessageRequest,
|
||||
SendDirectMessageRequest,
|
||||
)
|
||||
from app.routers.messages import send_channel_message, send_direct_message
|
||||
|
||||
|
||||
@@ -133,7 +139,12 @@ class TestOutgoingChannelBotTrigger:
|
||||
"app.repository.ChannelRepository.get_by_key",
|
||||
new=AsyncMock(return_value=db_channel),
|
||||
),
|
||||
patch(
|
||||
"app.repository.AppSettingsRepository.get",
|
||||
new=AsyncMock(return_value=AppSettings()),
|
||||
),
|
||||
patch("app.repository.MessageRepository.create", new=AsyncMock(return_value=1)),
|
||||
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()) as mock_bot,
|
||||
):
|
||||
@@ -165,7 +176,12 @@ class TestOutgoingChannelBotTrigger:
|
||||
"app.repository.ChannelRepository.get_by_key",
|
||||
new=AsyncMock(return_value=db_channel),
|
||||
),
|
||||
patch(
|
||||
"app.repository.AppSettingsRepository.get",
|
||||
new=AsyncMock(return_value=AppSettings()),
|
||||
),
|
||||
patch("app.repository.MessageRepository.create", new=AsyncMock(return_value=1)),
|
||||
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()) as mock_bot,
|
||||
):
|
||||
@@ -193,10 +209,99 @@ class TestOutgoingChannelBotTrigger:
|
||||
"app.repository.ChannelRepository.get_by_key",
|
||||
new=AsyncMock(return_value=db_channel),
|
||||
),
|
||||
patch(
|
||||
"app.repository.AppSettingsRepository.get",
|
||||
new=AsyncMock(return_value=AppSettings()),
|
||||
),
|
||||
patch("app.repository.MessageRepository.create", new=AsyncMock(return_value=1)),
|
||||
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=slow_bot),
|
||||
):
|
||||
request = SendChannelMessageRequest(channel_key=db_channel.key, text="test")
|
||||
message = await send_channel_message(request)
|
||||
assert message.outgoing is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_channel_msg_double_send_when_experimental_enabled(self):
|
||||
"""Experimental setting triggers an immediate byte-perfect duplicate send."""
|
||||
mc = _make_mc(name="MyNode")
|
||||
db_channel = Channel(key="dd" * 16, name="#double")
|
||||
settings = AppSettings(experimental_channel_double_send=True)
|
||||
|
||||
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.AppSettingsRepository.get", new=AsyncMock(return_value=settings)),
|
||||
patch("app.repository.MessageRepository.create", new=AsyncMock(return_value=1)),
|
||||
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.sleep", new=AsyncMock()) as mock_sleep,
|
||||
):
|
||||
request = SendChannelMessageRequest(channel_key=db_channel.key, text="same bytes")
|
||||
await send_channel_message(request)
|
||||
|
||||
assert mc.commands.send_chan_msg.await_count == 2
|
||||
mock_sleep.assert_awaited_once_with(3)
|
||||
first_call = mc.commands.send_chan_msg.await_args_list[0].kwargs
|
||||
second_call = mc.commands.send_chan_msg.await_args_list[1].kwargs
|
||||
assert first_call["chan"] == second_call["chan"]
|
||||
assert first_call["msg"] == second_call["msg"]
|
||||
assert first_call["timestamp"] == second_call["timestamp"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_channel_msg_single_send_when_experimental_disabled(self):
|
||||
"""Default setting keeps channel sends to a single radio command."""
|
||||
mc = _make_mc(name="MyNode")
|
||||
db_channel = Channel(key="ee" * 16, name="#single")
|
||||
|
||||
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.AppSettingsRepository.get",
|
||||
new=AsyncMock(return_value=AppSettings()),
|
||||
),
|
||||
patch("app.repository.MessageRepository.create", new=AsyncMock(return_value=1)),
|
||||
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()),
|
||||
):
|
||||
request = SendChannelMessageRequest(channel_key=db_channel.key, text="single send")
|
||||
await send_channel_message(request)
|
||||
|
||||
assert mc.commands.send_chan_msg.await_count == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_channel_msg_response_includes_current_ack_count(self):
|
||||
"""Send response reflects latest DB ack count at response time."""
|
||||
mc = _make_mc(name="MyNode")
|
||||
db_channel = Channel(key="ff" * 16, name="#acked")
|
||||
|
||||
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.AppSettingsRepository.get",
|
||||
new=AsyncMock(return_value=AppSettings()),
|
||||
),
|
||||
patch("app.repository.MessageRepository.create", new=AsyncMock(return_value=123)),
|
||||
patch("app.repository.MessageRepository.get_ack_count", new=AsyncMock(return_value=2)),
|
||||
patch("app.decoder.calculate_channel_hash", return_value="abcd"),
|
||||
patch("app.bot.run_bot_for_message", new=AsyncMock()),
|
||||
):
|
||||
request = SendChannelMessageRequest(channel_key=db_channel.key, text="acked now")
|
||||
message = await send_channel_message(request)
|
||||
|
||||
assert message.id == 123
|
||||
assert message.acked == 2
|
||||
|
||||
@@ -21,9 +21,11 @@ def _settings(
|
||||
favorites: list[Favorite] | None = None,
|
||||
migrated: bool = False,
|
||||
max_radio_contacts: int = 200,
|
||||
experimental_channel_double_send: bool = False,
|
||||
) -> AppSettings:
|
||||
return AppSettings(
|
||||
max_radio_contacts=max_radio_contacts,
|
||||
experimental_channel_double_send=experimental_channel_double_send,
|
||||
favorites=favorites or [],
|
||||
auto_decrypt_dm_on_advert=False,
|
||||
sidebar_sort_order="recent",
|
||||
@@ -45,7 +47,11 @@ class TestUpdateSettings:
|
||||
return_value=updated,
|
||||
) as mock_update:
|
||||
result = await update_settings(
|
||||
AppSettingsUpdate(max_radio_contacts=321, advert_interval=3600)
|
||||
AppSettingsUpdate(
|
||||
max_radio_contacts=321,
|
||||
advert_interval=3600,
|
||||
experimental_channel_double_send=True,
|
||||
)
|
||||
)
|
||||
|
||||
assert result.max_radio_contacts == 321
|
||||
@@ -53,6 +59,7 @@ class TestUpdateSettings:
|
||||
assert mock_update.call_args.kwargs == {
|
||||
"max_radio_contacts": 321,
|
||||
"advert_interval": 3600,
|
||||
"experimental_channel_double_send": True,
|
||||
}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user