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
|
|
@ -25,7 +25,6 @@ export interface ConnectionConfig {
|
||||||
serial_port: string
|
serial_port: string
|
||||||
tcp_host: string
|
tcp_host: string
|
||||||
tcp_port: number
|
tcp_port: number
|
||||||
transport?: string
|
|
||||||
meshcore_host?: string
|
meshcore_host?: string
|
||||||
meshcore_port?: number
|
meshcore_port?: number
|
||||||
}
|
}
|
||||||
|
|
@ -751,14 +750,14 @@ export function ConnectionSection({ data, onChange }: { data: ConnectionConfig;
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* MeshCore transport + host/port live on their own first-class page
|
{/* MeshCore host/port live on their own first-class page
|
||||||
(/meshcore/connection). Subtle cross-link only — no editable fields here. */}
|
(/meshcore/connection). Subtle cross-link only — no editable fields here. */}
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
<Link
|
<Link
|
||||||
to="/meshcore/connection"
|
to="/meshcore/connection"
|
||||||
className="inline-flex items-center gap-1 text-xs text-slate-500 hover:text-accent transition-colors"
|
className="inline-flex items-center gap-1 text-xs text-slate-500 hover:text-accent transition-colors"
|
||||||
>
|
>
|
||||||
→ MeshCore transport & connection
|
→ MeshCore connection
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ interface ConnectionConfig {
|
||||||
serial_port?: string
|
serial_port?: string
|
||||||
tcp_host?: string
|
tcp_host?: string
|
||||||
tcp_port?: number
|
tcp_port?: number
|
||||||
transport?: string
|
|
||||||
meshcore_host?: string
|
meshcore_host?: string
|
||||||
meshcore_port?: number
|
meshcore_port?: number
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
|
|
@ -159,54 +158,28 @@ export default function MeshCoreConnection() {
|
||||||
|
|
||||||
{/* Form */}
|
{/* Form */}
|
||||||
<div className="bg-bg-card border border-border p-6 space-y-4">
|
<div className="bg-bg-card border border-border p-6 space-y-4">
|
||||||
<div className="flex items-center justify-between py-2">
|
<div className="text-xs text-slate-500 uppercase tracking-wide">MeshCore Connection</div>
|
||||||
<div>
|
<p className="text-xs text-slate-500">
|
||||||
<span className="text-sm text-slate-300">Enable MeshCore</span>
|
Set the host and port to enable MeshCore; leave host blank to disable.
|
||||||
<p className="text-xs text-slate-600">
|
Meshtastic is always active.
|
||||||
Meshtastic is always on; enabling adds MeshCore (Both).
|
</p>
|
||||||
</p>
|
<div className="grid grid-cols-2 gap-4">
|
||||||
</div>
|
<TextInput
|
||||||
<button
|
label="MeshCore Host"
|
||||||
type="button"
|
value={config.meshcore_host ?? ''}
|
||||||
onClick={() => {
|
onChange={(v) => upd({ meshcore_host: v })}
|
||||||
const checked = !(config.transport === 'both' || config.transport === 'meshcore')
|
placeholder="192.168.1.100"
|
||||||
upd({ transport: checked ? 'both' : 'meshtastic' })
|
helper="IP or hostname — leave blank to disable MeshCore"
|
||||||
}}
|
info="MeshCore is active when this field is non-empty."
|
||||||
className={`relative w-11 h-6 rounded-full transition-colors ${
|
/>
|
||||||
config.transport === 'both' || config.transport === 'meshcore'
|
<NumberInput
|
||||||
? 'bg-accent'
|
label="MeshCore Port"
|
||||||
: 'bg-[#1e2a3a]'
|
value={config.meshcore_port ?? 5525}
|
||||||
}`}
|
onChange={(v) => upd({ meshcore_port: v })}
|
||||||
>
|
min={1}
|
||||||
<span
|
max={65535}
|
||||||
className={`absolute top-1 left-1 w-4 h-4 rounded-full bg-white transition-transform ${
|
helper="MeshCore TCP port (default 5525)"
|
||||||
config.transport === 'both' || config.transport === 'meshcore'
|
/>
|
||||||
? 'translate-x-5'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="pt-2 border-t border-[#1e2a3a] space-y-4">
|
|
||||||
<div className="text-xs text-slate-500 uppercase tracking-wide">MeshCore Connection</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<TextInput
|
|
||||||
label="MeshCore Host"
|
|
||||||
value={config.meshcore_host ?? ''}
|
|
||||||
onChange={(v) => upd({ meshcore_host: v })}
|
|
||||||
placeholder="192.168.1.100"
|
|
||||||
helper="IP or hostname of the MeshCore node"
|
|
||||||
info="Address of the MeshCore node to connect to."
|
|
||||||
/>
|
|
||||||
<NumberInput
|
|
||||||
label="MeshCore Port"
|
|
||||||
value={config.meshcore_port ?? 5525}
|
|
||||||
onChange={(v) => upd({ meshcore_port: v })}
|
|
||||||
min={1}
|
|
||||||
max={65535}
|
|
||||||
helper="MeshCore TCP port (default 5525)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
<Link
|
<Link
|
||||||
|
|
|
||||||
|
|
@ -34,12 +34,11 @@ class ConnectionConfig:
|
||||||
reconnect_initial_delay: float = 2.0
|
reconnect_initial_delay: float = 2.0
|
||||||
reconnect_max_delay: float = 60.0
|
reconnect_max_delay: float = 60.0
|
||||||
reconnect_health_interval: float = 30.0
|
reconnect_health_interval: float = 30.0
|
||||||
# --- transport selection (Phase 1 seam; MeshCore support is Phase 2) ---
|
|
||||||
transport: str = "meshtastic" # "meshtastic" | "meshcore" | "both"
|
|
||||||
# Universal mesh message budget (MeshCore LCD → 140 for all transports).
|
# Universal mesh message budget (MeshCore LCD → 140 for all transports).
|
||||||
mesh_max_chars: int = 140
|
mesh_max_chars: int = 140
|
||||||
# --- MeshCore transport settings (used when transport="meshcore") ---
|
# --- MeshCore transport settings ---
|
||||||
meshcore_host: str = "100.64.0.9" # pyMC companion frame server host
|
# MeshCore is active when meshcore_host is a non-empty string; blank = off.
|
||||||
|
meshcore_host: str = "" # pyMC companion frame server host
|
||||||
meshcore_port: int = 5050 # pyMC companion frame server port
|
meshcore_port: int = 5050 # pyMC companion frame server port
|
||||||
meshcore_auto_reconnect: bool = True # enable meshcore lib auto-reconnect
|
meshcore_auto_reconnect: bool = True # enable meshcore lib auto-reconnect
|
||||||
meshcore_max_reconnect_attempts: int = 5 # max reconnect attempts (0 = unlimited)
|
meshcore_max_reconnect_attempts: int = 5 # max reconnect attempts (0 = unlimited)
|
||||||
|
|
|
||||||
|
|
@ -394,7 +394,7 @@ class MeshAI:
|
||||||
# Load persisted summaries into memory cache
|
# Load persisted summaries into memory cache
|
||||||
await self._load_summaries()
|
await self._load_summaries()
|
||||||
|
|
||||||
# Transport connector (factory selects backend from config.connection.transport)
|
# Transport connector (factory derives backend from config.connection.meshcore_host)
|
||||||
self.connector = build_transport(self.config.connection)
|
self.connector = build_transport(self.config.connection)
|
||||||
|
|
||||||
# Fit every broadcast handler's one-packet formatter to the active mesh
|
# Fit every broadcast handler's one-packet formatter to the active mesh
|
||||||
|
|
|
||||||
|
|
@ -250,7 +250,7 @@ class MeshCoreBroadcastChannel(NotificationChannel):
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No MeshCore transport available",
|
"message": "No MeshCore transport available",
|
||||||
"error": "Set connection.transport to 'meshcore' or 'both'",
|
"error": "Set connection.meshcore_host to enable MeshCore",
|
||||||
"details": {"meshcore_channel": self._meshcore_channel},
|
"details": {"meshcore_channel": self._meshcore_channel},
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
@ -469,7 +469,7 @@ class MeshCoreDMChannel(NotificationChannel):
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No MeshCore transport available",
|
"message": "No MeshCore transport available",
|
||||||
"error": "Set connection.transport to 'meshcore' or 'both'",
|
"error": "Set connection.meshcore_host to enable MeshCore",
|
||||||
"details": {"contacts": self._contacts},
|
"details": {"contacts": self._contacts},
|
||||||
}
|
}
|
||||||
if not self._contacts:
|
if not self._contacts:
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,9 @@ Holds an ordered list of child transports and:
|
||||||
- self-filters inbound messages per child (drops own echoes);
|
- self-filters inbound messages per child (drops own echoes);
|
||||||
- exposes ``meshtastic_child()`` for the supervisor watchdog.
|
- exposes ``meshtastic_child()`` for the supervisor watchdog.
|
||||||
|
|
||||||
This transport is DORMANT unless ``transport: both`` in config. Single-
|
This transport is active when ``meshcore_host`` is configured (non-empty).
|
||||||
transport paths (``transport: meshtastic`` / ``transport: meshcore``) are
|
The factory instantiates it only when MeshCore is configured; Meshtastic-only
|
||||||
byte-identical to Phase 3 behaviour because the factory never instantiates
|
configs receive a bare MeshtasticTransport instead.
|
||||||
this class for them.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
|
||||||
|
|
@ -4,44 +4,29 @@ from .base import MeshTransport
|
||||||
|
|
||||||
|
|
||||||
def build_transport(config) -> MeshTransport:
|
def build_transport(config) -> MeshTransport:
|
||||||
"""Instantiate and return the configured MeshTransport.
|
"""Instantiate and return the active MeshTransport derived from config.
|
||||||
|
|
||||||
|
The active transports are derived from the connection config, not a
|
||||||
|
separate mode flag:
|
||||||
|
|
||||||
|
- Meshtastic is always the base transport.
|
||||||
|
- MeshCore is active when ``config.meshcore_host`` is a non-empty string.
|
||||||
|
- Both configured → CompositeTransport wrapping both.
|
||||||
|
- MeshCore host blank (default) → Meshtastic only.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: A ConnectionConfig (or duck-compatible object). The
|
config: A ConnectionConfig (or duck-compatible object).
|
||||||
``transport`` field selects the backend; it defaults to
|
|
||||||
``"meshtastic"`` so existing configs require no changes.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A concrete MeshTransport instance ready to be connected.
|
A concrete MeshTransport instance ready to be connected.
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: When the transport name is unrecognised.
|
|
||||||
"""
|
"""
|
||||||
transport_name = getattr(config, "transport", "meshtastic")
|
from meshai.connector import MeshtasticTransport
|
||||||
|
meshtastic = MeshtasticTransport(config)
|
||||||
|
|
||||||
if transport_name == "meshtastic":
|
meshcore_host = getattr(config, "meshcore_host", "") or ""
|
||||||
# Import here to avoid a circular import at module load time.
|
if meshcore_host.strip():
|
||||||
from meshai.connector import MeshtasticTransport
|
|
||||||
return MeshtasticTransport(config)
|
|
||||||
|
|
||||||
if transport_name == "meshcore":
|
|
||||||
# Function-local import keeps the module importable without the meshcore
|
|
||||||
# lib installed (lazy import pattern mirrors the meshtastic case).
|
|
||||||
from meshai.transport.meshcore_transport import MeshCoreTransport
|
|
||||||
return MeshCoreTransport(config)
|
|
||||||
|
|
||||||
if transport_name == "both":
|
|
||||||
# Phase 4: composite transport — drives Meshtastic and MeshCore
|
|
||||||
# simultaneously with correct reply routing.
|
|
||||||
from meshai.connector import MeshtasticTransport
|
|
||||||
from meshai.transport.meshcore_transport import MeshCoreTransport
|
from meshai.transport.meshcore_transport import MeshCoreTransport
|
||||||
from meshai.transport.composite_transport import CompositeTransport
|
from meshai.transport.composite_transport import CompositeTransport
|
||||||
return CompositeTransport([
|
return CompositeTransport([meshtastic, MeshCoreTransport(config)], config=config)
|
||||||
MeshtasticTransport(config),
|
|
||||||
MeshCoreTransport(config),
|
|
||||||
], config=config)
|
|
||||||
|
|
||||||
raise ValueError(
|
return meshtastic
|
||||||
f"Unknown transport {transport_name!r}. "
|
|
||||||
"Expected one of: 'meshtastic', 'meshcore', 'both'."
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,9 @@ class MeshCoreTransport(MeshTransport):
|
||||||
The meshcore client (``self._mc``) is created and used exclusively on that
|
The meshcore client (``self._mc``) is created and used exclusively on that
|
||||||
loop.
|
loop.
|
||||||
|
|
||||||
The transport is dormant until ``connect()`` is called. When
|
The transport is dormant until ``connect()`` is called. The factory
|
||||||
``transport != "meshcore"`` in config the factory never instantiates this
|
only instantiates this class when ``meshcore_host`` is non-empty, so
|
||||||
class, so there is zero cost to existing meshtastic deployments.
|
there is zero cost to Meshtastic-only deployments.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Name tag used by CompositeTransport for routing hints.
|
# Name tag used by CompositeTransport for routing hints.
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ class TestMaxChars:
|
||||||
def test_fixed_universal_budget_from_config(self) -> None:
|
def test_fixed_universal_budget_from_config(self) -> None:
|
||||||
"""With a config, CompositeTransport reads mesh_max_chars directly."""
|
"""With a config, CompositeTransport reads mesh_max_chars directly."""
|
||||||
from meshai.config import ConnectionConfig
|
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)
|
mt = FakeChild("meshtastic", max_chars_val=200)
|
||||||
mc = FakeChild("meshcore", max_chars_val=140)
|
mc = FakeChild("meshcore", max_chars_val=140)
|
||||||
comp = CompositeTransport([mt, mc], config=cfg)
|
comp = CompositeTransport([mt, mc], config=cfg)
|
||||||
|
|
@ -151,7 +151,7 @@ class TestMaxChars:
|
||||||
def test_fixed_universal_budget_ignores_child_values(self) -> None:
|
def test_fixed_universal_budget_ignores_child_values(self) -> None:
|
||||||
"""CompositeTransport must NOT take min(children); it sources config."""
|
"""CompositeTransport must NOT take min(children); it sources config."""
|
||||||
from meshai.config import ConnectionConfig
|
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.
|
# Even if a child would report 230, the composite must return 140.
|
||||||
child = FakeChild("meshtastic", max_chars_val=230)
|
child = FakeChild("meshtastic", max_chars_val=230)
|
||||||
comp = CompositeTransport([child], config=cfg)
|
comp = CompositeTransport([child], config=cfg)
|
||||||
|
|
|
||||||
|
|
@ -94,9 +94,8 @@ from meshai.transport.meshcore_transport import MeshCoreTransport # noqa: E402
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _mc_config(**overrides):
|
def _mc_config(**overrides):
|
||||||
"""Return a ConnectionConfig wired for meshcore."""
|
"""Return a ConnectionConfig with meshcore_host set (activates MeshCore)."""
|
||||||
cfg = ConnectionConfig(
|
cfg = ConnectionConfig(
|
||||||
transport="meshcore",
|
|
||||||
meshcore_host="127.0.0.1",
|
meshcore_host="127.0.0.1",
|
||||||
meshcore_port=5050,
|
meshcore_port=5050,
|
||||||
)
|
)
|
||||||
|
|
@ -186,11 +185,16 @@ def _install_channel_table(mc, table=None):
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
class TestBuildTransport:
|
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())
|
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):
|
def test_is_mesh_transport_subclass(self):
|
||||||
|
"""CompositeTransport (returned when meshcore_host is set) is a MeshTransport."""
|
||||||
t = build_transport(_mc_config())
|
t = build_transport(_mc_config())
|
||||||
assert isinstance(t, MeshTransport)
|
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
|
- MeshtasticTransport is a concrete subclass of MeshTransport
|
||||||
- build_transport returns the right type for the default config
|
- build_transport derives active transports from meshcore_host, not a flag
|
||||||
- build_transport raises appropriately for unimplemented / unknown transports
|
- MeshMessage has the expected transport-routing fields with correct defaults
|
||||||
- MeshMessage has the new additive fields with the expected defaults
|
|
||||||
|
|
||||||
No real radio, socket, or asyncio loop is required.
|
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:
|
class TestBuildTransport:
|
||||||
def _default_config(self):
|
def _config_no_meshcore(self):
|
||||||
return ConnectionConfig() # transport defaults to "meshtastic"
|
"""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 = ConnectionConfig()
|
||||||
cfg.transport = transport_name
|
cfg.meshcore_host = host
|
||||||
return cfg
|
return cfg
|
||||||
|
|
||||||
def test_default_config_returns_meshtastic_transport(self):
|
def test_blank_host_returns_meshtastic_only(self):
|
||||||
cfg = self._default_config()
|
"""Empty meshcore_host → MeshtasticTransport, no composite."""
|
||||||
|
cfg = self._config_no_meshcore()
|
||||||
|
assert cfg.meshcore_host == ""
|
||||||
transport = build_transport(cfg)
|
transport = build_transport(cfg)
|
||||||
assert isinstance(transport, MeshtasticTransport)
|
assert isinstance(transport, MeshtasticTransport)
|
||||||
|
# Must NOT be a composite when MeshCore is not configured.
|
||||||
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)."""
|
|
||||||
from meshai.transport.composite_transport import CompositeTransport
|
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)
|
t = build_transport(cfg)
|
||||||
assert isinstance(t, CompositeTransport)
|
assert isinstance(t, CompositeTransport)
|
||||||
assert len(t.children) == 2
|
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):
|
def test_whitespace_only_host_is_treated_as_blank(self):
|
||||||
cfg = self._config_with("unknown_transport_xyz")
|
"""Whitespace-only meshcore_host is treated as blank → Meshtastic only."""
|
||||||
with pytest.raises(ValueError):
|
cfg = ConnectionConfig()
|
||||||
build_transport(cfg)
|
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):
|
def _mt_config(**overrides):
|
||||||
"""Return a ConnectionConfig wired for meshtastic (default)."""
|
"""Return a ConnectionConfig with no MeshCore host (Meshtastic-only)."""
|
||||||
cfg = ConnectionConfig(transport="meshtastic")
|
cfg = ConnectionConfig() # meshcore_host defaults to "" → Meshtastic only
|
||||||
for k, v in overrides.items():
|
for k, v in overrides.items():
|
||||||
setattr(cfg, k, v)
|
setattr(cfg, k, v)
|
||||||
return cfg
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
def _mc_config(**overrides):
|
def _mc_config(**overrides):
|
||||||
"""Return a ConnectionConfig wired for meshcore."""
|
"""Return a ConnectionConfig with meshcore_host set (activates MeshCore)."""
|
||||||
cfg = ConnectionConfig(
|
cfg = ConnectionConfig(
|
||||||
transport="meshcore",
|
|
||||||
meshcore_host="127.0.0.1",
|
meshcore_host="127.0.0.1",
|
||||||
meshcore_port=5050,
|
meshcore_port=5050,
|
||||||
)
|
)
|
||||||
|
|
@ -158,7 +157,7 @@ class TestTransportMaxChars:
|
||||||
"""Sanity: all transports + CompositeTransport report mesh_max_chars=140.
|
"""Sanity: all transports + CompositeTransport report mesh_max_chars=140.
|
||||||
|
|
||||||
This is the canonical single-budget guarantee: Meshtastic, MeshCore,
|
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.config import ConnectionConfig
|
||||||
from meshai.transport.composite_transport import CompositeTransport
|
from meshai.transport.composite_transport import CompositeTransport
|
||||||
|
|
@ -169,9 +168,8 @@ class TestTransportMaxChars:
|
||||||
# MeshCore
|
# MeshCore
|
||||||
assert MeshCoreTransport(_mc_config()).max_chars == 140
|
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(
|
cfg_both = ConnectionConfig(
|
||||||
transport="both",
|
|
||||||
type="tcp",
|
type="tcp",
|
||||||
tcp_host="127.0.0.1",
|
tcp_host="127.0.0.1",
|
||||||
tcp_port=4403,
|
tcp_port=4403,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue