2026-07-02 12:33:17 -06:00
|
|
|
"""Tests for uniform mesh-packet sizing.
|
2026-07-02 10:46:11 -06:00
|
|
|
|
2026-07-02 12:33:17 -06:00
|
|
|
Verifies that every size-sensitive code path uses the single universal
|
|
|
|
|
mesh_max_chars constant (140 = MeshCore LCD). All transports — Meshtastic,
|
|
|
|
|
MeshCore, and the composite — report the same fixed value.
|
2026-07-02 10:46:11 -06:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import sys
|
|
|
|
|
import types
|
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Fake meshcore module (must precede production imports that lazy-import it)
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
def _build_fake_meshcore():
|
|
|
|
|
mod = types.ModuleType("meshcore")
|
|
|
|
|
|
|
|
|
|
class EventType:
|
|
|
|
|
CONTACT_MSG_RECV = "CONTACT_MSG_RECV"
|
|
|
|
|
CHANNEL_MSG_RECV = "CHANNEL_MSG_RECV"
|
|
|
|
|
DISCONNECTED = "DISCONNECTED"
|
|
|
|
|
CONNECTED = "CONNECTED"
|
|
|
|
|
|
|
|
|
|
mod.EventType = EventType
|
|
|
|
|
|
|
|
|
|
class _FakeMeshCore:
|
|
|
|
|
self_info = {"public_key": "aabbccdd1122", "name": "FakeNode"}
|
|
|
|
|
contacts = {}
|
|
|
|
|
|
|
|
|
|
async def start_auto_message_fetching(self):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
async def stop_auto_message_fetching(self):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
async def disconnect(self):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def subscribe(self, event_type, callback):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def get_contact_by_key_prefix(self, prefix):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
async def create_tcp(cls, host, port,
|
|
|
|
|
auto_reconnect=True, max_reconnect_attempts=5):
|
|
|
|
|
return cls()
|
|
|
|
|
|
|
|
|
|
class commands:
|
|
|
|
|
@staticmethod
|
|
|
|
|
async def send_chan_msg(chan_idx, text):
|
|
|
|
|
result = MagicMock()
|
|
|
|
|
result.is_error.return_value = False
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
async def send_msg(dst, text):
|
|
|
|
|
result = MagicMock()
|
|
|
|
|
result.is_error.return_value = False
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
mod.MeshCore = _FakeMeshCore
|
|
|
|
|
return mod
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sys.modules.setdefault("meshcore", _build_fake_meshcore())
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Production imports
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
from meshai.config import ConnectionConfig # noqa: E402
|
|
|
|
|
from meshai.connector import MeshtasticTransport # noqa: E402
|
|
|
|
|
from meshai.transport.meshcore_transport import MeshCoreTransport # noqa: E402
|
|
|
|
|
from meshai.transport.factory import build_transport # noqa: E402
|
|
|
|
|
from meshai.notifications.channels import ( # noqa: E402
|
|
|
|
|
MeshBroadcastChannel,
|
|
|
|
|
MeshDMChannel,
|
|
|
|
|
)
|
|
|
|
|
from meshai.notifications.pipeline import build_pipeline # noqa: E402
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Helpers
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
def _mt_config(**overrides):
|
2026-07-03 00:18:53 -06:00
|
|
|
"""Return a ConnectionConfig with no MeshCore host (Meshtastic-only)."""
|
|
|
|
|
cfg = ConnectionConfig() # meshcore_host defaults to "" → Meshtastic only
|
2026-07-02 10:46:11 -06:00
|
|
|
for k, v in overrides.items():
|
|
|
|
|
setattr(cfg, k, v)
|
|
|
|
|
return cfg
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _mc_config(**overrides):
|
2026-07-03 00:18:53 -06:00
|
|
|
"""Return a ConnectionConfig with meshcore_host set (activates MeshCore)."""
|
2026-07-02 10:46:11 -06:00
|
|
|
cfg = ConnectionConfig(
|
|
|
|
|
meshcore_host="127.0.0.1",
|
|
|
|
|
meshcore_port=5050,
|
|
|
|
|
)
|
|
|
|
|
for k, v in overrides.items():
|
|
|
|
|
setattr(cfg, k, v)
|
|
|
|
|
return cfg
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _fake_connector(max_chars_val: int):
|
|
|
|
|
"""Minimal fake connector exposing only max_chars."""
|
|
|
|
|
conn = MagicMock()
|
|
|
|
|
conn.max_chars = max_chars_val
|
|
|
|
|
return conn
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _minimal_config():
|
|
|
|
|
"""Minimal full Config for build_pipeline calls."""
|
|
|
|
|
from meshai.config import Config
|
|
|
|
|
return Config()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# 1. Transport max_chars property
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
class TestTransportMaxChars:
|
2026-07-02 12:33:17 -06:00
|
|
|
def test_meshtastic_default_is_140(self):
|
|
|
|
|
"""Meshtastic now uses the universal mesh_max_chars constant (140)."""
|
2026-07-02 10:46:11 -06:00
|
|
|
cfg = _mt_config()
|
|
|
|
|
t = MeshtasticTransport(cfg)
|
2026-07-02 12:33:17 -06:00
|
|
|
assert t.max_chars == 140
|
2026-07-02 10:46:11 -06:00
|
|
|
|
|
|
|
|
def test_meshcore_default_is_140(self):
|
|
|
|
|
cfg = _mc_config()
|
|
|
|
|
t = MeshCoreTransport(cfg)
|
|
|
|
|
assert t.max_chars == 140
|
|
|
|
|
|
|
|
|
|
def test_build_transport_meshtastic_max_chars(self):
|
2026-07-02 12:33:17 -06:00
|
|
|
"""build_transport(meshtastic) → 140 (universal constant)."""
|
2026-07-02 10:46:11 -06:00
|
|
|
t = build_transport(_mt_config())
|
2026-07-02 12:33:17 -06:00
|
|
|
assert t.max_chars == 140
|
2026-07-02 10:46:11 -06:00
|
|
|
|
|
|
|
|
def test_build_transport_meshcore_max_chars(self):
|
|
|
|
|
t = build_transport(_mc_config())
|
|
|
|
|
assert t.max_chars == 140
|
|
|
|
|
|
2026-07-02 12:33:17 -06:00
|
|
|
def test_mesh_max_chars_config_field_governs_all(self):
|
|
|
|
|
"""A non-default mesh_max_chars propagates to both transport types."""
|
|
|
|
|
cfg_mt = _mt_config(mesh_max_chars=120)
|
|
|
|
|
assert MeshtasticTransport(cfg_mt).max_chars == 120
|
|
|
|
|
|
|
|
|
|
cfg_mc = _mc_config(mesh_max_chars=120)
|
|
|
|
|
assert MeshCoreTransport(cfg_mc).max_chars == 120
|
|
|
|
|
|
|
|
|
|
def test_all_three_transports_and_composite_report_140(self):
|
|
|
|
|
"""Sanity: all transports + CompositeTransport report mesh_max_chars=140.
|
|
|
|
|
|
|
|
|
|
This is the canonical single-budget guarantee: Meshtastic, MeshCore,
|
2026-07-03 00:18:53 -06:00
|
|
|
and the composite (meshcore_host set) all resolve to the same constant.
|
2026-07-02 12:33:17 -06:00
|
|
|
"""
|
|
|
|
|
from meshai.config import ConnectionConfig
|
|
|
|
|
from meshai.transport.composite_transport import CompositeTransport
|
|
|
|
|
|
|
|
|
|
# Meshtastic
|
|
|
|
|
assert MeshtasticTransport(_mt_config()).max_chars == 140
|
|
|
|
|
|
|
|
|
|
# MeshCore
|
|
|
|
|
assert MeshCoreTransport(_mc_config()).max_chars == 140
|
|
|
|
|
|
2026-07-03 00:18:53 -06:00
|
|
|
# Composite — derived from meshcore_host being non-empty
|
2026-07-02 12:33:17 -06:00
|
|
|
cfg_both = ConnectionConfig(
|
|
|
|
|
type="tcp",
|
|
|
|
|
tcp_host="127.0.0.1",
|
|
|
|
|
tcp_port=4403,
|
|
|
|
|
meshcore_host="127.0.0.1",
|
|
|
|
|
meshcore_port=5050,
|
|
|
|
|
)
|
|
|
|
|
comp = build_transport(cfg_both)
|
|
|
|
|
assert isinstance(comp, CompositeTransport)
|
|
|
|
|
assert comp.max_chars == 140
|
|
|
|
|
|
2026-07-02 10:46:11 -06:00
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# 2. MeshRenderer char_limit propagation via channels
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
class TestChannelRendererBudget:
|
2026-07-02 12:33:17 -06:00
|
|
|
def test_broadcast_channel_propagates_connector_max_chars(self):
|
|
|
|
|
"""Channel renderer inherits max_chars from whatever the connector reports."""
|
2026-07-02 10:46:11 -06:00
|
|
|
conn = _fake_connector(140)
|
|
|
|
|
ch = MeshBroadcastChannel(connector=conn, channel_index=0)
|
|
|
|
|
assert ch._renderer._limit == 140
|
|
|
|
|
|
2026-07-02 12:33:17 -06:00
|
|
|
def test_broadcast_channel_with_meshtastic_connector_uses_140(self):
|
|
|
|
|
"""After the universal budget refactor, a meshtastic connector reports 140."""
|
|
|
|
|
conn = _fake_connector(140)
|
2026-07-02 10:46:11 -06:00
|
|
|
ch = MeshBroadcastChannel(connector=conn, channel_index=0)
|
2026-07-02 12:33:17 -06:00
|
|
|
assert ch._renderer._limit == 140
|
2026-07-02 10:46:11 -06:00
|
|
|
|
2026-07-02 12:33:17 -06:00
|
|
|
def test_dm_channel_propagates_connector_max_chars(self):
|
2026-07-02 10:46:11 -06:00
|
|
|
conn = _fake_connector(140)
|
|
|
|
|
ch = MeshDMChannel(connector=conn, node_ids=["!aabbccdd"])
|
|
|
|
|
assert ch._renderer._limit == 140
|
|
|
|
|
|
2026-07-02 12:33:17 -06:00
|
|
|
def test_dm_channel_with_meshtastic_connector_uses_140(self):
|
|
|
|
|
conn = _fake_connector(140)
|
2026-07-02 10:46:11 -06:00
|
|
|
ch = MeshDMChannel(connector=conn, node_ids=["!aabbccdd"])
|
2026-07-02 12:33:17 -06:00
|
|
|
assert ch._renderer._limit == 140
|
2026-07-02 10:46:11 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# 3. DigestAccumulator mesh_char_limit propagation via build_pipeline
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
class TestBuildPipelineMeshCharLimit:
|
2026-07-02 12:33:17 -06:00
|
|
|
def test_no_connector_uses_140(self):
|
|
|
|
|
"""Without a connector the pipeline falls back to the universal constant (140)."""
|
2026-07-02 10:46:11 -06:00
|
|
|
cfg = _minimal_config()
|
|
|
|
|
bus = build_pipeline(cfg, llm_backend=None, connector=None)
|
|
|
|
|
acc = bus._pipeline_components["accumulator"]
|
2026-07-02 12:33:17 -06:00
|
|
|
assert acc._mesh_char_limit == 140
|
2026-07-02 10:46:11 -06:00
|
|
|
|
|
|
|
|
def test_meshcore_connector_uses_140(self):
|
|
|
|
|
cfg = _minimal_config()
|
|
|
|
|
conn = _fake_connector(140)
|
|
|
|
|
bus = build_pipeline(cfg, llm_backend=None, connector=conn)
|
|
|
|
|
acc = bus._pipeline_components["accumulator"]
|
|
|
|
|
assert acc._mesh_char_limit == 140
|
|
|
|
|
|
2026-07-02 12:33:17 -06:00
|
|
|
def test_meshtastic_connector_uses_140(self):
|
|
|
|
|
"""After universal budget, a meshtastic connector also reports 140."""
|
2026-07-02 10:46:11 -06:00
|
|
|
cfg = _minimal_config()
|
2026-07-02 12:33:17 -06:00
|
|
|
conn = _fake_connector(140)
|
2026-07-02 10:46:11 -06:00
|
|
|
bus = build_pipeline(cfg, llm_backend=None, connector=conn)
|
|
|
|
|
acc = bus._pipeline_components["accumulator"]
|
2026-07-02 12:33:17 -06:00
|
|
|
assert acc._mesh_char_limit == 140
|
2026-07-02 10:46:11 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# 4. Runtime-override durability: survives adapter_config cache invalidation
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
class TestRuntimeOverrideDurability:
|
|
|
|
|
"""Verify set_runtime_override writes are not cleared by invalidate_cache().
|
|
|
|
|
|
|
|
|
|
We test at the _resolve/_overrides seam directly because the full
|
|
|
|
|
adapter_config.nws.single_packet_max_chars read path hits the DB, which
|
|
|
|
|
is not available in this hermetic test environment. Testing via _resolve
|
|
|
|
|
is equivalent: it is the exact call site used by _AdapterSection.__getattr__.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def test_override_survives_cache_invalidation(self):
|
|
|
|
|
from meshai.adapter_config._accessor import (
|
|
|
|
|
_overrides,
|
|
|
|
|
_resolve,
|
|
|
|
|
set_runtime_override,
|
|
|
|
|
invalidate_cache,
|
|
|
|
|
_SENTINEL,
|
|
|
|
|
)
|
|
|
|
|
key = ("nws", "single_packet_max_chars")
|
|
|
|
|
try:
|
|
|
|
|
set_runtime_override("nws", "single_packet_max_chars", 140)
|
|
|
|
|
|
|
|
|
|
# Override is visible through the resolution path.
|
|
|
|
|
assert _resolve("nws", "single_packet_max_chars") == 140
|
|
|
|
|
|
|
|
|
|
# Cache invalidation must NOT wipe the override.
|
|
|
|
|
invalidate_cache()
|
|
|
|
|
assert _resolve("nws", "single_packet_max_chars") == 140
|
|
|
|
|
finally:
|
|
|
|
|
# Clean up so this test cannot pollute subsequent tests.
|
|
|
|
|
_overrides.pop(key, None)
|