fix(template): stop pathbytes_min reading a hopless packet as multibyte

{path_distance|pathbytes_min:2|prefix_if_nonempty: | Path Dist: } printed
"| Path Dist: N/A" on a direct message. bytes_per_hop_from_routing_and_nodes
returned routing_info's bytes_per_hop before it ever looked at whether there
were any hops, so a 0-hop packet whose format happens to use 2-byte hops
reported 2, the gate passed, and prefix_if_nonempty found "N/A" waiting for
it and dutifully attached the label.

bytes_per_hop describes how a path is encoded. A direct packet has no path
for it to describe, so the format field is not evidence of a wide path and
must not stand in for one. Both docstrings already promised 1 for the direct
case—"Returns 1 when no nodes (direct / unknown)"—so this is the code
catching up to its stated contract rather than a change of policy.

Latent until now: channel messages never had routing_info attached, so the
field was absent and the fallback returned 1 by accident. Direct DMs have
been hitting it all along.
This commit is contained in:
agessaman committed 2026-08-25 09:39:29 -07:00
1 parent ba23899c31
commit f9f65037a8
3 files changed
+70 -5

No files matched your search

+8
View File
@@ -8,6 +8,14 @@ semantic versioning.
### Fixed
- `pathbytes_min` no longer treats a direct message as a multi-byte path, so
`{path_distance|pathbytes_min:2|prefix_if_nonempty: | Path Dist: }` stops printing
`| Path Dist: N/A` on a hopless packet. `bytes_per_hop` describes how a path is
encoded, and a direct packet has no path for it to describe, so
`bytes_per_hop_from_routing_and_nodes` now returns 1 whenever the packet reports no
hops—which is what its docstring already claimed. The bug was latent until channel
messages started carrying routing info.
- Channel messages are now tied to their own RF packet, so `{packet_hash}`, `{path}`
and `{connection_info}` resolve on a channel instead of coming back empty or
`Unknown`. MeshCore's CHAN event carries neither `raw_hex` nor a pubkey prefix, so
+15 -5
View File
@@ -1892,10 +1892,20 @@ def bytes_per_hop_from_routing_and_nodes(
) -> int:
"""Bytes per hop from packet routing metadata, else inferred from hex node width.
When ``routing_info`` includes ``bytes_per_hop`` in 1..3, that value wins.
Otherwise uses minimum half-byte width among ``node_ids`` (comma or path_nodes).
Returns ``1`` when no nodes (direct / unknown).
When the packet carries a path and ``routing_info`` includes ``bytes_per_hop``
in 1..3, that value wins. Otherwise uses minimum half-byte width among
``node_ids`` (comma or path_nodes). Returns ``1`` when no nodes (direct /
unknown).
A hopless packet always reports ``1``, whatever ``bytes_per_hop`` says.
``bytes_per_hop`` describes how a path is *encoded*, and a direct packet has no
path for it to describe, so letting the format field through would make
``pathbytes_min:2`` treat "Direct" as a multi-byte path and print a label for a
distance that does not exist.
"""
path_length = (routing_info or {}).get('path_length')
if not node_ids and not path_length:
return 1
if routing_info:
bph = routing_info.get('bytes_per_hop')
if isinstance(bph, int) and 1 <= bph <= 3:
@@ -1912,8 +1922,8 @@ def message_path_bytes_per_hop(message: Any, *, prefix_hex_chars: int = 2) -> in
:func:`extract_path_node_ids_from_message`, then comma/continuous hex via
:func:`node_ids_from_path_string` using ``prefix_hex_chars`` for legacy paths.
Returns ``1`` when no usable path (direct / unparseable) so conservative gates
(e.g. ``pathbytes_min:2``) do not treat unknown as multibyte.
Returns ``1`` when there is no usable path (direct / unparseable) so conservative
gates (e.g. ``pathbytes_min:2``) treat neither unknown nor hopless as multibyte.
"""
routing_info = getattr(message, 'routing_info', None)
node_ids = extract_path_node_ids_from_message(message)
+47
View File
@@ -22,6 +22,53 @@ def test_message_path_bytes_per_hop_from_routing():
assert message_path_bytes_per_hop(msg) == 2
@pytest.mark.unit
def test_hopless_packet_is_never_multibyte():
"""bytes_per_hop describes how a path is encoded; a direct packet has no path for
it to describe, so pathbytes_min must not read the format field as a wide path."""
msg = MeshMessage(
content="test",
channel="c",
path="Direct",
routing_info={"bytes_per_hop": 2, "path_length": 0, "path_nodes": []},
)
assert message_path_bytes_per_hop(msg) == 1
@pytest.mark.unit
def test_pathbytes_min_hides_label_on_a_direct_message():
"""A direct message has no path distance, so the whole clause must disappear
rather than render "Path Dist: N/A"."""
msg = MeshMessage(
content="test",
channel="c",
path="Direct",
routing_info={"bytes_per_hop": 2, "path_length": 0, "path_nodes": []},
)
out = format_piped_template(
"ack{path_distance|pathbytes_min:2|prefix_if_nonempty: | Path Dist: }",
{"path_distance": "N/A"},
message=msg,
)
assert out == "ack"
@pytest.mark.unit
def test_pathbytes_min_still_passes_a_real_multibyte_path():
msg = MeshMessage(
content="test",
channel="c",
path="7a2a,0102 (2 hops)",
routing_info={"bytes_per_hop": 2, "path_length": 2, "path_nodes": ["7A2A", "0102"]},
)
out = format_piped_template(
"ack{path_distance|pathbytes_min:2|prefix_if_nonempty: | Path Dist: }",
{"path_distance": "12.4km"},
message=msg,
)
assert out == "ack | Path Dist: 12.4km"
@pytest.mark.unit
def test_message_path_bytes_per_hop_infers_from_nodes():
msg = MeshMessage(