mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
refactor(transport): derive active transports from config, drop transport setting (#13)
* refactor(transport): derive active transports from config, drop transport setting A mesh is active when its connection is configured: Meshtastic is the always-on base; MeshCore runs whenever meshcore_host is set (blank = off); both configured = both. Removes the transport mode field/toggle entirely so there's no separate flag to miss. * docs: fix stale transport comment after field removal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b260dcbae0
commit
47b56adab5
12 changed files with 128 additions and 144 deletions
|
|
@ -142,7 +142,7 @@ class TestMaxChars:
|
|||
def test_fixed_universal_budget_from_config(self) -> None:
|
||||
"""With a config, CompositeTransport reads mesh_max_chars directly."""
|
||||
from meshai.config import ConnectionConfig
|
||||
cfg = ConnectionConfig(transport="both", mesh_max_chars=140)
|
||||
cfg = ConnectionConfig(mesh_max_chars=140)
|
||||
mt = FakeChild("meshtastic", max_chars_val=200)
|
||||
mc = FakeChild("meshcore", max_chars_val=140)
|
||||
comp = CompositeTransport([mt, mc], config=cfg)
|
||||
|
|
@ -151,7 +151,7 @@ class TestMaxChars:
|
|||
def test_fixed_universal_budget_ignores_child_values(self) -> None:
|
||||
"""CompositeTransport must NOT take min(children); it sources config."""
|
||||
from meshai.config import ConnectionConfig
|
||||
cfg = ConnectionConfig(transport="both", mesh_max_chars=140)
|
||||
cfg = ConnectionConfig(mesh_max_chars=140)
|
||||
# Even if a child would report 230, the composite must return 140.
|
||||
child = FakeChild("meshtastic", max_chars_val=230)
|
||||
comp = CompositeTransport([child], config=cfg)
|
||||
|
|
|
|||
|
|
@ -94,9 +94,8 @@ from meshai.transport.meshcore_transport import MeshCoreTransport # noqa: E402
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _mc_config(**overrides):
|
||||
"""Return a ConnectionConfig wired for meshcore."""
|
||||
"""Return a ConnectionConfig with meshcore_host set (activates MeshCore)."""
|
||||
cfg = ConnectionConfig(
|
||||
transport="meshcore",
|
||||
meshcore_host="127.0.0.1",
|
||||
meshcore_port=5050,
|
||||
)
|
||||
|
|
@ -186,11 +185,16 @@ def _install_channel_table(mc, table=None):
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestBuildTransport:
|
||||
def test_returns_meshcore_transport(self):
|
||||
def test_returns_composite_when_meshcore_host_set(self):
|
||||
"""meshcore_host set → build_transport returns CompositeTransport."""
|
||||
from meshai.transport.composite_transport import CompositeTransport
|
||||
t = build_transport(_mc_config())
|
||||
assert isinstance(t, MeshCoreTransport)
|
||||
assert isinstance(t, CompositeTransport)
|
||||
# MeshCoreTransport must be the second child.
|
||||
assert isinstance(t.children[1], MeshCoreTransport)
|
||||
|
||||
def test_is_mesh_transport_subclass(self):
|
||||
"""CompositeTransport (returned when meshcore_host is set) is a MeshTransport."""
|
||||
t = build_transport(_mc_config())
|
||||
assert isinstance(t, MeshTransport)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
"""Lightweight tests for the Phase-1 MeshTransport abstraction.
|
||||
"""Tests for the MeshTransport abstraction and derive-from-config factory.
|
||||
|
||||
These tests exercise the structural contracts introduced by the refactor:
|
||||
These tests exercise:
|
||||
- MeshtasticTransport is a concrete subclass of MeshTransport
|
||||
- build_transport returns the right type for the default config
|
||||
- build_transport raises appropriately for unimplemented / unknown transports
|
||||
- MeshMessage has the new additive fields with the expected defaults
|
||||
- build_transport derives active transports from meshcore_host, not a flag
|
||||
- MeshMessage has the expected transport-routing fields with correct defaults
|
||||
|
||||
No real radio, socket, or asyncio loop is required.
|
||||
"""
|
||||
|
|
@ -47,50 +46,78 @@ class TestMeshtasticTransportABC:
|
|||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Factory
|
||||
# Factory — transports derived from meshcore_host, not a transport flag
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestBuildTransport:
|
||||
def _default_config(self):
|
||||
return ConnectionConfig() # transport defaults to "meshtastic"
|
||||
def _config_no_meshcore(self):
|
||||
"""Default config: meshcore_host is blank → Meshtastic only."""
|
||||
return ConnectionConfig()
|
||||
|
||||
def _config_with(self, transport_name):
|
||||
def _config_with_meshcore(self, host="1.2.3.4"):
|
||||
"""Config with a non-empty meshcore_host → composite."""
|
||||
cfg = ConnectionConfig()
|
||||
cfg.transport = transport_name
|
||||
cfg.meshcore_host = host
|
||||
return cfg
|
||||
|
||||
def test_default_config_returns_meshtastic_transport(self):
|
||||
cfg = self._default_config()
|
||||
def test_blank_host_returns_meshtastic_only(self):
|
||||
"""Empty meshcore_host → MeshtasticTransport, no composite."""
|
||||
cfg = self._config_no_meshcore()
|
||||
assert cfg.meshcore_host == ""
|
||||
transport = build_transport(cfg)
|
||||
assert isinstance(transport, MeshtasticTransport)
|
||||
|
||||
def test_explicit_meshtastic_returns_meshtastic_transport(self):
|
||||
cfg = self._config_with("meshtastic")
|
||||
transport = build_transport(cfg)
|
||||
assert isinstance(transport, MeshtasticTransport)
|
||||
|
||||
def test_meshcore_returns_meshcore_transport(self):
|
||||
# Phase 2: meshcore is now implemented; build_transport returns a
|
||||
# MeshCoreTransport instance (MeshTransport subclass).
|
||||
from meshai.transport.meshcore_transport import MeshCoreTransport
|
||||
cfg = self._config_with("meshcore")
|
||||
transport = build_transport(cfg)
|
||||
assert isinstance(transport, MeshCoreTransport)
|
||||
assert isinstance(transport, MeshTransport)
|
||||
|
||||
def test_both_returns_composite(self):
|
||||
"""Phase 4: transport='both' now returns a CompositeTransport (seam filled)."""
|
||||
# Must NOT be a composite when MeshCore is not configured.
|
||||
from meshai.transport.composite_transport import CompositeTransport
|
||||
cfg = self._config_with("both")
|
||||
assert not isinstance(transport, CompositeTransport)
|
||||
|
||||
def test_meshcore_host_set_returns_composite(self):
|
||||
"""Non-empty meshcore_host → CompositeTransport with both children."""
|
||||
from meshai.transport.composite_transport import CompositeTransport
|
||||
from meshai.transport.meshcore_transport import MeshCoreTransport
|
||||
cfg = self._config_with_meshcore("1.2.3.4")
|
||||
t = build_transport(cfg)
|
||||
assert isinstance(t, CompositeTransport)
|
||||
assert len(t.children) == 2
|
||||
assert isinstance(t.children[0], MeshtasticTransport)
|
||||
assert isinstance(t.children[1], MeshCoreTransport)
|
||||
|
||||
def test_unknown_transport_raises_value_error(self):
|
||||
cfg = self._config_with("unknown_transport_xyz")
|
||||
with pytest.raises(ValueError):
|
||||
build_transport(cfg)
|
||||
def test_whitespace_only_host_is_treated_as_blank(self):
|
||||
"""Whitespace-only meshcore_host is treated as blank → Meshtastic only."""
|
||||
cfg = ConnectionConfig()
|
||||
cfg.meshcore_host = " "
|
||||
transport = build_transport(cfg)
|
||||
assert isinstance(transport, MeshtasticTransport)
|
||||
from meshai.transport.composite_transport import CompositeTransport
|
||||
assert not isinstance(transport, CompositeTransport)
|
||||
|
||||
def test_config_has_no_transport_field(self):
|
||||
"""ConnectionConfig must not have a transport attribute after the refactor."""
|
||||
cfg = ConnectionConfig()
|
||||
assert not hasattr(cfg, "transport"), (
|
||||
"ConnectionConfig.transport was not removed; the refactor is incomplete."
|
||||
)
|
||||
|
||||
def test_stray_transport_key_in_yaml_loads_without_error(self):
|
||||
"""A config dict with a stale 'transport' key must load cleanly."""
|
||||
from meshai.config import _dict_to_dataclass, ConnectionConfig as CC
|
||||
raw = {
|
||||
"type": "tcp",
|
||||
"tcp_host": "10.0.0.1",
|
||||
"transport": "both", # stale key — must be ignored
|
||||
"meshcore_host": "192.168.1.253",
|
||||
}
|
||||
cfg = _dict_to_dataclass(CC, raw)
|
||||
# The stale key must be silently dropped; no AttributeError.
|
||||
assert not hasattr(cfg, "transport")
|
||||
assert cfg.meshcore_host == "192.168.1.253"
|
||||
|
||||
def test_serialized_config_has_no_transport_field(self):
|
||||
"""_dataclass_to_dict must not emit a 'transport' key."""
|
||||
from meshai.config import _dataclass_to_dict
|
||||
cfg = ConnectionConfig()
|
||||
d = _dataclass_to_dict(cfg)
|
||||
assert "transport" not in d
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -89,17 +89,16 @@ from meshai.notifications.pipeline import build_pipeline # noqa: E402
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _mt_config(**overrides):
|
||||
"""Return a ConnectionConfig wired for meshtastic (default)."""
|
||||
cfg = ConnectionConfig(transport="meshtastic")
|
||||
"""Return a ConnectionConfig with no MeshCore host (Meshtastic-only)."""
|
||||
cfg = ConnectionConfig() # meshcore_host defaults to "" → Meshtastic only
|
||||
for k, v in overrides.items():
|
||||
setattr(cfg, k, v)
|
||||
return cfg
|
||||
|
||||
|
||||
def _mc_config(**overrides):
|
||||
"""Return a ConnectionConfig wired for meshcore."""
|
||||
"""Return a ConnectionConfig with meshcore_host set (activates MeshCore)."""
|
||||
cfg = ConnectionConfig(
|
||||
transport="meshcore",
|
||||
meshcore_host="127.0.0.1",
|
||||
meshcore_port=5050,
|
||||
)
|
||||
|
|
@ -158,7 +157,7 @@ class TestTransportMaxChars:
|
|||
"""Sanity: all transports + CompositeTransport report mesh_max_chars=140.
|
||||
|
||||
This is the canonical single-budget guarantee: Meshtastic, MeshCore,
|
||||
and the composite (transport=both) all resolve to the same constant.
|
||||
and the composite (meshcore_host set) all resolve to the same constant.
|
||||
"""
|
||||
from meshai.config import ConnectionConfig
|
||||
from meshai.transport.composite_transport import CompositeTransport
|
||||
|
|
@ -169,9 +168,8 @@ class TestTransportMaxChars:
|
|||
# MeshCore
|
||||
assert MeshCoreTransport(_mc_config()).max_chars == 140
|
||||
|
||||
# Composite (built via factory with transport="both")
|
||||
# Composite — derived from meshcore_host being non-empty
|
||||
cfg_both = ConnectionConfig(
|
||||
transport="both",
|
||||
type="tcp",
|
||||
tcp_host="127.0.0.1",
|
||||
tcp_port=4403,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue