mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
refactor(sizing): single mesh packet budget via transport.max_chars (Phase 3) (#5)
Route all mesh message sizing (renderer, digest, reply chunker, NWS one-packet fit) through the active transport's max_chars instead of scattered literal 200s. Meshtastic pinned at 200 (byte-identical output); MeshCore uses its configured ~140. Sets up uniform-to-smaller sizing for the composite transport. No behavior change on the Meshtastic path. Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
316ae7351e
commit
225a5d37df
12 changed files with 292 additions and 8 deletions
244
work/tests/test_uniform_sizing.py
Normal file
244
work/tests/test_uniform_sizing.py
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
"""Tests for Phase 3 uniform mesh-packet sizing.
|
||||
|
||||
Verifies that every size-sensitive code path uses the active transport's
|
||||
max_chars rather than a hardcoded 200. Meshtastic default stays 200
|
||||
(byte-identical); MeshCore uses 140.
|
||||
"""
|
||||
|
||||
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):
|
||||
"""Return a ConnectionConfig wired for meshtastic (default)."""
|
||||
cfg = ConnectionConfig(transport="meshtastic")
|
||||
for k, v in overrides.items():
|
||||
setattr(cfg, k, v)
|
||||
return cfg
|
||||
|
||||
|
||||
def _mc_config(**overrides):
|
||||
"""Return a ConnectionConfig wired for meshcore."""
|
||||
cfg = ConnectionConfig(
|
||||
transport="meshcore",
|
||||
meshcore_host="127.0.0.1",
|
||||
meshcore_port=5050,
|
||||
meshcore_channel_index=0,
|
||||
)
|
||||
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:
|
||||
def test_meshtastic_default_is_200(self):
|
||||
cfg = _mt_config()
|
||||
t = MeshtasticTransport(cfg)
|
||||
assert t.max_chars == 200
|
||||
|
||||
def test_meshtastic_respects_config_field(self):
|
||||
cfg = _mt_config(meshtastic_max_chars=160)
|
||||
t = MeshtasticTransport(cfg)
|
||||
assert t.max_chars == 160
|
||||
|
||||
def test_meshcore_default_is_140(self):
|
||||
cfg = _mc_config()
|
||||
t = MeshCoreTransport(cfg)
|
||||
assert t.max_chars == 140
|
||||
|
||||
def test_meshcore_respects_config_field(self):
|
||||
cfg = _mc_config(meshcore_max_chars=120)
|
||||
t = MeshCoreTransport(cfg)
|
||||
assert t.max_chars == 120
|
||||
|
||||
def test_build_transport_meshtastic_max_chars(self):
|
||||
t = build_transport(_mt_config())
|
||||
assert t.max_chars == 200
|
||||
|
||||
def test_build_transport_meshcore_max_chars(self):
|
||||
t = build_transport(_mc_config())
|
||||
assert t.max_chars == 140
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. MeshRenderer char_limit propagation via channels
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestChannelRendererBudget:
|
||||
def test_broadcast_channel_with_meshcore_connector_uses_140(self):
|
||||
conn = _fake_connector(140)
|
||||
ch = MeshBroadcastChannel(connector=conn, channel_index=0)
|
||||
assert ch._renderer._limit == 140
|
||||
|
||||
def test_broadcast_channel_with_meshtastic_connector_uses_200(self):
|
||||
conn = _fake_connector(200)
|
||||
ch = MeshBroadcastChannel(connector=conn, channel_index=0)
|
||||
assert ch._renderer._limit == 200
|
||||
|
||||
def test_dm_channel_with_meshcore_connector_uses_140(self):
|
||||
conn = _fake_connector(140)
|
||||
ch = MeshDMChannel(connector=conn, node_ids=["!aabbccdd"])
|
||||
assert ch._renderer._limit == 140
|
||||
|
||||
def test_dm_channel_with_meshtastic_connector_uses_200(self):
|
||||
conn = _fake_connector(200)
|
||||
ch = MeshDMChannel(connector=conn, node_ids=["!aabbccdd"])
|
||||
assert ch._renderer._limit == 200
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. DigestAccumulator mesh_char_limit propagation via build_pipeline
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestBuildPipelineMeshCharLimit:
|
||||
def test_no_connector_uses_200(self):
|
||||
cfg = _minimal_config()
|
||||
bus = build_pipeline(cfg, llm_backend=None, connector=None)
|
||||
acc = bus._pipeline_components["accumulator"]
|
||||
assert acc._mesh_char_limit == 200
|
||||
|
||||
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
|
||||
|
||||
def test_meshtastic_connector_uses_200(self):
|
||||
cfg = _minimal_config()
|
||||
conn = _fake_connector(200)
|
||||
bus = build_pipeline(cfg, llm_backend=None, connector=conn)
|
||||
acc = bus._pipeline_components["accumulator"]
|
||||
assert acc._mesh_char_limit == 200
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue