fix(companion): protect direct offline messages

This commit is contained in:
agessaman
2026-07-14 16:59:34 -07:00
parent 1fe3fb1779
commit 79cba76b4d
4 changed files with 132 additions and 22 deletions
+19 -1
View File
@@ -5,7 +5,7 @@ from typing import Any, cast
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from openhop_core.companion.constants import RESP_CODE_NO_MORE_MESSAGES
from openhop_core.companion.constants import PUSH_CODE_MSG_WAITING, RESP_CODE_NO_MORE_MESSAGES
from repeater.companion.bridge import RepeaterCompanionBridge, _to_json_safe
from repeater.companion.frame_server import CompanionFrameServer
@@ -201,6 +201,24 @@ async def test_frame_server_no_more_messages_response_when_empty():
assert srv._write_frame.call_args[0][0] == bytes([RESP_CODE_NO_MORE_MESSAGES])
@pytest.mark.asyncio
async def test_rejected_queue_callback_skips_sqlite_persistence_but_notifies_client():
server = object.__new__(CompanionFrameServer)
server._persist_companion_message = AsyncMock()
server._enqueue_frame = MagicMock()
await server._on_message_received(
b"\x01" * 32,
"rejected",
1,
0,
queued=False,
)
server._persist_companion_message.assert_not_awaited()
server._enqueue_frame.assert_called_once_with(bytes([PUSH_CODE_MSG_WAITING]))
def test_companion_utils_validation_and_normalization():
assert normalize_companion_identity_key(" 0xAABB ") == "AABB"
assert validate_companion_node_name(" node-1 ") == "node-1"
+56 -3
View File
@@ -176,9 +176,54 @@ class TestSqliteRetentionTrim:
def test_trims_to_max_messages(self, tmp_path):
h = self._handler(tmp_path)
for i in range(5):
self._push(h, "0x01", i, max_messages=3)
assert len(h.companion_load_messages("0x01")) == 3
results = [self._push(h, "0x01", i, max_messages=3) for i in range(5)]
assert results == [True, True, True, False, False]
assert [m["text"] for m in h.companion_load_messages("0x01")] == ["m0", "m1", "m2"]
def test_evicts_oldest_channel_message_before_direct_message(self, tmp_path):
h = self._handler(tmp_path)
direct_one = {"text": "direct one", "packet_hash": "d1", "is_channel": False}
channel_one = {"text": "channel one", "packet_hash": "c1", "is_channel": True}
direct_two = {"text": "direct two", "packet_hash": "d2", "is_channel": False}
assert h.companion_push_message("0x01", direct_one, max_messages=2)
assert h.companion_push_message("0x01", channel_one, max_messages=2)
assert h.companion_push_message("0x01", direct_two, max_messages=2)
messages = h.companion_load_messages("0x01")
assert [m["text"] for m in messages] == ["direct one", "direct two"]
assert [m["is_channel"] for m in messages] == [0, 0]
def test_rejects_channel_when_queue_contains_only_direct_messages(self, tmp_path):
h = self._handler(tmp_path)
for packet_hash in ("d1", "d2"):
assert h.companion_push_message(
"0x01", {"text": packet_hash, "packet_hash": packet_hash}, max_messages=2
)
assert not h.companion_push_message(
"0x01", {"text": "channel", "packet_hash": "c1", "is_channel": True}, max_messages=2
)
assert [m["text"] for m in h.companion_load_messages("0x01")] == ["d1", "d2"]
def test_rejected_insert_keeps_existing_channels_when_limit_is_lowered(self, tmp_path):
h = self._handler(tmp_path)
existing = [
{"text": "direct one", "packet_hash": "d1", "is_channel": False},
{"text": "channel one", "packet_hash": "c1", "is_channel": True},
{"text": "direct two", "packet_hash": "d2", "is_channel": False},
]
for message in existing:
assert h.companion_push_message("0x01", message)
assert not h.companion_push_message(
"0x01", {"text": "incoming", "packet_hash": "d3"}, max_messages=2
)
assert [m["text"] for m in h.companion_load_messages("0x01")] == [
"direct one",
"channel one",
"direct two",
]
def test_none_keeps_all(self, tmp_path):
h = self._handler(tmp_path)
@@ -381,6 +426,14 @@ class TestPersistSkipWhenOff:
asyncio.run(fs._persist_companion_message({"text": "x"}))
fs.sqlite_handler.companion_push_message.assert_called_once_with("0x01", {"text": "x"}, 7)
def test_keeps_memory_message_when_sqlite_rejects_it(self):
import asyncio
fs = self._frame_server(7)
fs.sqlite_handler.companion_push_message.return_value = False
asyncio.run(fs._persist_companion_message({"text": "x"}))
fs.bridge.message_queue.pop_last.assert_not_called()
class TestImportRepeaterContactsCap:
"""The import endpoint must never leave persisted contacts above max_contacts.