Files
meshcore-bot/tests/test_multitest_command.py
agessaman 43f450e702 Update path formatting and message handling in commands
- Modified path formatting in `multitest_command.py` to use ideographic space for nested branches, enhancing visual clarity.
- Updated the `schedule_command.py` to strip control characters from messages, ensuring safe and clean previews.
- Adjusted test cases in `test_multitest_command.py` to reflect changes in path formatting, improving consistency across tests.

These changes enhance the readability of path outputs and improve message handling in scheduled commands.
2026-03-29 20:27:18 -07:00

367 lines
12 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for modules.commands.multitest_command — pure logic functions."""
import configparser
from unittest.mock import MagicMock, Mock
from modules.commands.multitest_command import (
MultitestCommand,
_condense_path_lines,
_path_to_tokens,
)
from tests.conftest import mock_message
_INTER = "\u251c"
_LAST = "\u2514"
_INDENT = "\u3000" #   before nested ├/└
_CHILD_INTER = f"{_INDENT}{_INTER} "
_CHILD_LAST = f"{_INDENT}{_LAST} "
_CORNER = "\u2510" # ┐ after common path
def _make_bot():
bot = MagicMock()
bot.logger = Mock()
config = configparser.ConfigParser()
config.add_section("Bot")
config.set("Bot", "bot_name", "TestBot")
config.add_section("Channels")
config.set("Channels", "monitor_channels", "general")
config.set("Channels", "respond_to_dms", "true")
config.add_section("Keywords")
config.add_section("Multitest_Command")
config.set("Multitest_Command", "enabled", "true")
bot.config = config
bot.translator = MagicMock()
bot.translator.translate = Mock(side_effect=lambda key, **kw: key)
bot.command_manager = MagicMock()
bot.command_manager.monitor_channels = ["general"]
bot.prefix_hex_chars = 2
return bot
class TestExtractPathFromRfData:
"""Tests for extract_path_from_rf_data."""
def setup_method(self):
self.cmd = MultitestCommand(_make_bot())
def test_no_routing_info_returns_none(self):
result = self.cmd.extract_path_from_rf_data({})
assert result is None
def test_empty_routing_info_returns_none(self):
result = self.cmd.extract_path_from_rf_data({"routing_info": {}})
assert result is None
def test_path_nodes_extracted(self):
rf_data = {
"routing_info": {
"path_nodes": ["01", "7a", "55"]
}
}
result = self.cmd.extract_path_from_rf_data(rf_data)
assert result == "01,7a,55"
def test_path_hex_fallback(self):
rf_data = {
"routing_info": {
"path_nodes": [],
"path_hex": "017a55",
"bytes_per_hop": 1
}
}
result = self.cmd.extract_path_from_rf_data(rf_data)
assert result is not None
assert "01" in result
def test_invalid_nodes_skipped(self):
rf_data = {
"routing_info": {
"path_nodes": ["01", "zz", "55"]
}
}
result = self.cmd.extract_path_from_rf_data(rf_data)
# zz is invalid hex, should be excluded
if result:
assert "zz" not in result
class TestExtractPathFromMessage:
"""Tests for extract_path_from_message."""
def setup_method(self):
self.cmd = MultitestCommand(_make_bot())
def test_no_path_returns_none(self):
msg = mock_message(content="multitest", path=None)
msg.routing_info = None
result = self.cmd.extract_path_from_message(msg)
assert result is None
def test_direct_returns_none(self):
msg = mock_message(content="multitest", path="Direct")
msg.routing_info = None
result = self.cmd.extract_path_from_message(msg)
assert result is None
def test_zero_hops_returns_none(self):
msg = mock_message(content="multitest", path="0 hops")
msg.routing_info = None
result = self.cmd.extract_path_from_message(msg)
assert result is None
def test_comma_path_extracted(self):
msg = mock_message(content="multitest", path="01,7a,55")
msg.routing_info = None
result = self.cmd.extract_path_from_message(msg)
assert result is not None
assert "01" in result
def test_routing_info_path_preferred(self):
msg = mock_message(content="multitest", path="01,7a")
msg.routing_info = {
"path_length": 2,
"path_nodes": ["7a", "55"],
"bytes_per_hop": None
}
result = self.cmd.extract_path_from_message(msg)
# routing_info is preferred
assert result is not None
class TestMatchesKeyword:
"""Tests for matches_keyword."""
def setup_method(self):
self.cmd = MultitestCommand(_make_bot())
def test_multitest_matches(self):
assert self.cmd.matches_keyword(mock_message(content="multitest")) is True
def test_mt_matches(self):
assert self.cmd.matches_keyword(mock_message(content="mt")) is True
def test_exclamation_prefix(self):
assert self.cmd.matches_keyword(mock_message(content="!multitest")) is True
def test_other_does_not_match(self):
assert self.cmd.matches_keyword(mock_message(content="ping")) is False
class TestCondensePathLines:
"""Tests for _condense_path_lines."""
def test_meshed_up_style_strict_prefix_and_branches(self):
paths = sorted(
[
"e6,0c,85,82,28,1a,cd,7e,01",
"e6,0c,85,82,28,1a,cd,7e,7a",
"e6,0c,85,82,28,1a,cd,7e,7a,09",
"e6,0c,85,82,28,1a,cd",
]
)
out = _condense_path_lines(paths)
expected = "\n".join(
[
f"e6,0c,85,82,28,1a,cd,7e {_CORNER}",
f"{_INTER} 01",
f"{_INTER} 7a",
f"{_CHILD_INTER}09",
f"{_LAST} ...",
]
)
assert out == expected
def test_shared_prefix_no_strict_prefix_truncation(self):
paths = sorted(
[
"aa,bb,cc",
"aa,bb,cc,dd",
"aa,bb,cc,ee",
]
)
out = _condense_path_lines(paths)
# LCP shrinks so cc is not the whole “trunk” while dd/ee branch off
expected = "\n".join(
[
f"aa,bb {_CORNER}",
f"{_INTER} cc",
f"{_CHILD_INTER}dd",
f"{_CHILD_LAST}ee",
]
)
assert out == expected
def test_one_path_ends_at_lcp_other_extends(self):
"""Shorter LCP so both 0101 and 0101,0970 appear as branches, not one hidden on the trunk."""
paths = sorted(["cdf1,7e76,0101", "cdf1,7e76,0101,0970"])
out = _condense_path_lines(paths)
expected = "\n".join(
[
f"cdf1,7e76 {_CORNER}",
f"{_INTER} 0101",
f"{_CHILD_LAST}0970",
]
)
assert out == expected
def test_overlapping_suffix_branches_under_common_prefix(self):
paths = sorted(
[
"cdf119,860cca,010101",
"cdf119,860cca,e0eed9",
"cdf119,860cca,e0eed9,1ed612",
]
)
out = _condense_path_lines(paths)
expected = "\n".join(
[
f"cdf119,860cca {_CORNER}",
f"{_INTER} 010101",
f"{_INTER} e0eed9",
f"{_CHILD_LAST}1ed612",
]
)
assert out == expected
def test_divergent_routes_with_shared_mid_prefix(self):
"""TRM-style: in-group LCP 13,01 so 1e nests; 83,09 stays its own branch."""
paths = sorted(["41,96,13,01", "41,96,13,01,1e", "41,96,83,09"])
out = _condense_path_lines(paths)
expected = "\n".join(
[
f"41,96 {_CORNER}",
f"{_INTER} 13,01",
f"{_CHILD_INTER}1e",
f"{_LAST} 83,09",
]
)
assert out == expected
def test_shared_second_hop_not_shown_as_endpoint(self):
"""W7ZOO-style: 96,e0 shared by all variants; endpoints are 01,09,1e and fc,7a — not 96."""
paths = sorted(
[
"cc,fe,17,b3,7e,96,e0",
"cc,fe,17,b3,7e,96,e0,01",
"cc,fe,17,b3,7e,96,e0,09",
"cc,fe,17,b3,7e,96,e0,1e",
"cc,fe,17,b3,7e,fc,7a",
]
)
out = _condense_path_lines(paths)
expected = "\n".join(
[
f"cc,fe,17,b3,7e {_CORNER}",
f"{_INTER} 96,e0",
f"{_CHILD_INTER}01",
f"{_CHILD_INTER}09",
f"{_CHILD_INTER}1e",
f"{_LAST} fc,7a",
]
)
assert out == expected
def test_mixed_first_hops_nest_per_group(self):
"""Ill Eagle-style: 01 vs 01,1e share a group; 09 and e0 are separate top-level branches."""
paths = sorted(
[
"e2,ab,1f,ef,55,21,01",
"e2,ab,1f,ef,55,21,01,1e",
"e2,ab,1f,ef,55,21,09",
"e2,ab,1f,ef,55,21,e0",
]
)
out = _condense_path_lines(paths)
expected = "\n".join(
[
f"e2,ab,1f,ef,55,21 {_CORNER}",
f"{_INTER} 01",
f"{_CHILD_INTER}1e",
f"{_INTER} 09",
f"{_LAST} e0",
]
)
assert out == expected
def test_shorter_path_one_extra_hop_still_trees(self):
"""860cca vs 860cca,010101: shrink trunk so both show as branches."""
paths = sorted(
[
"d38a05,c4a86a,067b75,cafee0,1ffbd6,e8154b,860cca,010101",
"d38a05,c4a86a,067b75,cafee0,1ffbd6,e8154b,860cca",
]
)
out = _condense_path_lines(paths)
expected = "\n".join(
[
f"d38a05,c4a86a,067b75,cafee0,1ffbd6,e8154b {_CORNER}",
f"{_INTER} 860cca",
f"{_CHILD_LAST}010101",
]
)
assert out == expected
def test_shared_hop_then_horiz_continuations(self):
"""All paths share first hop after LCP → one ├ hop line then  ├/ └ remainders (U+3000)."""
paths = sorted(
[
"d38a05,479198,a837bc,7e7662,e0eed9",
"d38a05,479198,a837bc,7e7662,e0eed9,010101",
"d38a05,479198,a837bc,7e7662,e0eed9,0970d6",
"d38a05,479198,a837bc,7e7662,e0eed9,1ed612",
"d38a05,479198,a837bc,7e7662,e0eed9,f",
]
)
out = _condense_path_lines(paths)
expected = "\n".join(
[
f"d38a05,479198,a837bc,7e7662 {_CORNER}",
f"{_INTER} e0eed9",
f"{_CHILD_INTER}010101",
f"{_CHILD_INTER}0970d6",
f"{_CHILD_INTER}1ed612",
f"{_CHILD_LAST}f",
]
)
assert out == expected
def test_disjoint_first_hop_groups_with_brackets(self):
paths = sorted(["a,b", "c,d"])
out = _condense_path_lines(paths)
expected = "\n".join(["[a,b]", "[c,d]"])
assert out == expected
def test_single_path_unchanged(self):
assert _condense_path_lines(["a,b,c"]) == "a,b,c"
def test_path_to_tokens_strips_trailing_empty_segment(self):
assert _path_to_tokens("e6,0c,cd,") == ["e6", "0c", "cd"]
class TestCanExecute:
"""Tests for can_execute."""
def test_enabled(self):
bot = _make_bot()
cmd = MultitestCommand(bot)
msg = mock_message(content="multitest", channel="general")
assert cmd.can_execute(msg) is True
def test_disabled(self):
bot = _make_bot()
bot.config.set("Multitest_Command", "enabled", "false")
cmd = MultitestCommand(bot)
msg = mock_message(content="multitest", channel="general")
assert cmd.can_execute(msg) is False
def test_condense_paths_default_false(self):
cmd = MultitestCommand(_make_bot())
assert cmd.condense_paths is False
def test_condense_paths_true_from_config(self):
bot = _make_bot()
bot.config.set("Multitest_Command", "condense_paths", "true")
cmd = MultitestCommand(bot)
assert cmd.condense_paths is True