mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-08-10 19:03:22 +02:00
feat: add region/default-scope / cli commands update
standardize default region on mesh.default_region (remove legacy region_default_scope usage) add/align CLI support for owner.info, path.hash.mode, and loop.detect with validation wire UI terminal get/set + autocomplete/help for owner.info, path.hash.mode, loop.detect extend update_radio_config to persist owner_info for UI set owner.info add and use shared packet utility for advert creation + default-region transport scoping refactor repeater and room-server advert paths to reuse shared packet logic
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
from repeater.handler_helpers.mesh_cli import MeshCLI
|
||||
|
||||
|
||||
@@ -11,6 +10,7 @@ def _base_config():
|
||||
"repeater": {
|
||||
"name": "node-a",
|
||||
"mode": "forward",
|
||||
"owner_info": "owner-a",
|
||||
"latitude": 1.2,
|
||||
"longitude": 3.4,
|
||||
"airtime_factor": 1.1,
|
||||
@@ -31,6 +31,7 @@ def _base_config():
|
||||
"coding_rate": 5,
|
||||
"tx_power": 22,
|
||||
},
|
||||
"mesh": {"path_hash_mode": 0, "loop_detect": "minimal"},
|
||||
"security": {"guest_password": "guest", "allow_read_only": True},
|
||||
}
|
||||
|
||||
@@ -52,12 +53,13 @@ def test_handle_command_admin_and_prefix_behavior():
|
||||
def test_help_routing_and_basic_unknown_paths():
|
||||
cli = MeshCLI("/tmp/cfg.yaml", _base_config(), _cfg_mgr(), enable_regions=False)
|
||||
|
||||
assert "pyMC CLI Commands" in cli._route_command("help")
|
||||
assert "openHop CLI Commands" in cli._route_command("help")
|
||||
assert "No detailed help" in cli._route_command("help nope")
|
||||
assert cli._route_command("start ota").startswith("Error:")
|
||||
assert cli._route_command("sensor read").startswith("Error:")
|
||||
assert cli._route_command("gps on").startswith("Error:")
|
||||
assert cli._route_command("stats-foo").startswith("Error:")
|
||||
assert cli._route_command("discover.neighbors").startswith("Error:")
|
||||
assert cli._route_command("region load x").startswith("Error: Region commands not available")
|
||||
assert cli._route_command("unknown") == "Unknown command"
|
||||
|
||||
@@ -129,17 +131,24 @@ def test_cmd_get_public_key_and_neighbor_branches():
|
||||
storage.get_neighbors = lambda: {
|
||||
"aa": {"is_repeater": False, "zero_hop": False, "last_seen": 1}
|
||||
}
|
||||
assert "No repeaters or zero hop" in cli._cmd_neighbors()
|
||||
assert "No zero hop repeaters" in cli._cmd_neighbors()
|
||||
|
||||
storage.get_neighbors = lambda: {
|
||||
"abcdef12feed": {"is_repeater": True, "zero_hop": False, "last_seen": 10, "snr": 4.9},
|
||||
"11223344aabb": {"is_repeater": False, "zero_hop": True, "last_seen": 20, "snr": 1.2},
|
||||
"99aabbccddeeff00": {
|
||||
"is_repeater": True,
|
||||
"zero_hop": True,
|
||||
"last_seen": 21,
|
||||
"snr": 6.3,
|
||||
},
|
||||
}
|
||||
with patch("time.time", return_value=30):
|
||||
out = cli._cmd_neighbors()
|
||||
|
||||
assert "abcdef12:20:4" in out
|
||||
assert "11223344:10:1" in out
|
||||
assert "99aabbcc:9:6" in out
|
||||
assert "abcdef12:20:4" not in out
|
||||
assert "11223344:10:1" not in out
|
||||
|
||||
cli.storage_handler = SimpleNamespace(
|
||||
get_neighbors=MagicMock(side_effect=RuntimeError("db fail"))
|
||||
@@ -147,6 +156,14 @@ def test_cmd_get_public_key_and_neighbor_branches():
|
||||
assert cli._cmd_neighbors().startswith("Error:")
|
||||
|
||||
|
||||
def test_cmd_get_owner_info_path_hash_mode_and_loop_detect():
|
||||
cli = MeshCLI("/tmp/cfg.yaml", _base_config(), _cfg_mgr())
|
||||
|
||||
assert cli._cmd_get("owner.info") == "> owner-a"
|
||||
assert cli._cmd_get("path.hash.mode") == "> 0"
|
||||
assert cli._cmd_get("loop.detect") == "> minimal"
|
||||
|
||||
|
||||
def test_cmd_set_updates_and_validation_errors():
|
||||
cfg = _base_config()
|
||||
mgr = _cfg_mgr()
|
||||
@@ -167,7 +184,18 @@ def test_cmd_set_updates_and_validation_errors():
|
||||
assert cli._cmd_set("freq 868000000").startswith("OK")
|
||||
assert cli._cmd_set("tx 17") == "OK"
|
||||
assert cli._cmd_set("guest.password g") == "OK"
|
||||
assert cli._cmd_set("owner.info Alice|Ops") == "OK"
|
||||
assert cfg["repeater"]["owner_info"] == "Alice\nOps"
|
||||
assert cli._cmd_set("allow.read.only off") == "OK"
|
||||
assert cli._cmd_set("path.hash.mode 2") == "OK"
|
||||
assert cfg["mesh"]["path_hash_mode"] == 2
|
||||
assert cli._cmd_set("path.hash.mode 3") == "Error: path.hash.mode must be 0, 1, or 2"
|
||||
assert cli._cmd_set("loop.detect strict") == "OK"
|
||||
assert cfg["mesh"]["loop_detect"] == "strict"
|
||||
assert (
|
||||
cli._cmd_set("loop.detect weird")
|
||||
== "Error: loop.detect must be off, minimal, moderate, or strict"
|
||||
)
|
||||
|
||||
assert cli._cmd_set("advert.interval 59").startswith("Error: interval range")
|
||||
assert cli._cmd_set("flood.advert.interval 2").startswith("Error: interval range")
|
||||
@@ -185,10 +213,12 @@ def test_cmd_set_updates_and_validation_errors():
|
||||
def test_misc_commands_and_routes():
|
||||
cli = MeshCLI("/tmp/cfg.yaml", _base_config(), _cfg_mgr(), enable_regions=True)
|
||||
|
||||
assert cli._cmd_region("region").startswith("Error:")
|
||||
assert cli._cmd_region("region load us").startswith("Error:")
|
||||
assert cli._cmd_region("region save").startswith("Error:")
|
||||
assert cli._cmd_region("region remove x").startswith("Error:")
|
||||
cli.storage_handler = SimpleNamespace(get_transport_keys=lambda: [])
|
||||
|
||||
assert cli._cmd_region("region") == "* F"
|
||||
assert cli._cmd_region("region load us").startswith("Err -")
|
||||
assert cli._cmd_region("region save") == "OK"
|
||||
assert cli._cmd_region("region remove x") == "Err - not found"
|
||||
assert cli._cmd_region("region unknown").startswith("Err -")
|
||||
|
||||
assert cli._cmd_setperm("setperm") == "Err - bad params"
|
||||
@@ -204,11 +234,170 @@ def test_misc_commands_and_routes():
|
||||
assert cli._cmd_tempradio("tempradio 915 125 7 5 nope") == "Error, invalid params"
|
||||
assert cli._cmd_tempradio("tempradio 915 125 7 5 10").startswith("Error:")
|
||||
|
||||
assert cli._cmd_neighbor_remove("neighbor.remove ") == "ERR: Missing pubkey"
|
||||
assert cli._cmd_neighbor_remove("neighbor.remove abc").startswith("Error:")
|
||||
assert cli._cmd_neighbor_remove("neighbor.remove") == "ERR: Missing pubkey"
|
||||
|
||||
storage = SimpleNamespace(delete_neighbors_by_pubkey_prefix=MagicMock(return_value=1))
|
||||
cli.storage_handler = storage
|
||||
assert cli._cmd_neighbor_remove("neighbor.remove abc") == "OK"
|
||||
storage.delete_neighbors_by_pubkey_prefix.assert_called_with("abc")
|
||||
|
||||
assert cli._cmd_neighbor_remove("neighbor.remove ") == "OK"
|
||||
storage.delete_neighbors_by_pubkey_prefix.assert_called_with(None)
|
||||
|
||||
assert cli._cmd_neighbor_remove("neighbor.remove zz") == "ERR: bad pubkey"
|
||||
|
||||
no_delete = SimpleNamespace()
|
||||
cli.storage_handler = no_delete
|
||||
assert "not supported" in cli._cmd_neighbor_remove("neighbor.remove ab")
|
||||
|
||||
cli.storage_handler = None
|
||||
assert cli._cmd_neighbor_remove("neighbor.remove ab") == "Error: Storage not available"
|
||||
|
||||
discovery = SimpleNamespace(
|
||||
cleanup_sessions=MagicMock(),
|
||||
create_session=MagicMock(return_value={"session_id": "s1"}),
|
||||
start_session_task=MagicMock(),
|
||||
)
|
||||
loop = SimpleNamespace(is_running=lambda: True, call_soon_threadsafe=MagicMock())
|
||||
cli._event_loop = loop
|
||||
cli.config_manager.daemon = SimpleNamespace(discovery_helper=discovery)
|
||||
|
||||
assert cli._cmd_discover_neighbors("discover.neighbors") == "OK - Discover sent"
|
||||
discovery.cleanup_sessions.assert_called_once()
|
||||
assert callable(discovery.create_session.call_args.kwargs["result_enricher"])
|
||||
loop.call_soon_threadsafe.assert_called_once_with(discovery.start_session_task, "s1")
|
||||
|
||||
assert cli._cmd_discover_neighbors("discover.neighbors foo").startswith("Err -")
|
||||
|
||||
cli.config_manager.daemon = SimpleNamespace(discovery_helper=None)
|
||||
assert cli._cmd_discover_neighbors("discover.neighbors").startswith("Error: Discovery helper")
|
||||
|
||||
assert cli._cmd_log("log start").startswith("Error:")
|
||||
assert cli._cmd_log("log stop").startswith("Error:")
|
||||
assert cli._cmd_log("log erase").startswith("Error:")
|
||||
assert cli._cmd_log("log") == "Error: Use journalctl to view logs"
|
||||
assert cli._cmd_log("log whatever") == "Unknown log command"
|
||||
|
||||
|
||||
def test_region_command_response_shapes_and_ops():
|
||||
keys = [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "alpha",
|
||||
"flood_policy": "allow",
|
||||
"parent_id": None,
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "#beta",
|
||||
"flood_policy": "deny",
|
||||
"parent_id": 1,
|
||||
},
|
||||
]
|
||||
|
||||
created = []
|
||||
updated = []
|
||||
deleted = []
|
||||
|
||||
def get_transport_keys():
|
||||
return list(keys)
|
||||
|
||||
def update_transport_key(key_id, **kwargs):
|
||||
updated.append((key_id, kwargs))
|
||||
for rec in keys:
|
||||
if rec["id"] == key_id:
|
||||
rec.update(kwargs)
|
||||
return True
|
||||
return False
|
||||
|
||||
def create_transport_key(name, flood_policy, transport_key, parent_id, last_used):
|
||||
new_id = max([r["id"] for r in keys] + [0]) + 1
|
||||
keys.append(
|
||||
{
|
||||
"id": new_id,
|
||||
"name": name,
|
||||
"flood_policy": flood_policy,
|
||||
"parent_id": parent_id,
|
||||
}
|
||||
)
|
||||
created.append((name, flood_policy, parent_id))
|
||||
return new_id
|
||||
|
||||
def delete_transport_key(key_id):
|
||||
for idx, rec in enumerate(keys):
|
||||
if rec["id"] == key_id:
|
||||
deleted.append(key_id)
|
||||
del keys[idx]
|
||||
return True
|
||||
return False
|
||||
|
||||
cfg = _base_config()
|
||||
cfg["mesh"] = {"unscoped_flood_allow": True, "default_region": None}
|
||||
cli = MeshCLI("/tmp/cfg.yaml", cfg, _cfg_mgr(), enable_regions=True)
|
||||
cli.storage_handler = SimpleNamespace(
|
||||
get_transport_keys=get_transport_keys,
|
||||
update_transport_key=update_transport_key,
|
||||
create_transport_key=create_transport_key,
|
||||
delete_transport_key=delete_transport_key,
|
||||
)
|
||||
|
||||
tree = cli._cmd_region("region")
|
||||
assert "* F" in tree
|
||||
assert " alpha F" in tree
|
||||
assert " beta" in tree
|
||||
|
||||
assert cli._cmd_region("region get alpha") == " alpha F"
|
||||
assert cli._cmd_region("region get beta") == " beta (alpha)"
|
||||
assert cli._cmd_region("region get missing") == "Err - unknown region"
|
||||
|
||||
assert cli._cmd_region("region allowf beta") == "OK"
|
||||
assert keys[1]["flood_policy"] == "allow"
|
||||
assert cli._cmd_region("region denyf beta") == "OK"
|
||||
assert keys[1]["flood_policy"] == "deny"
|
||||
|
||||
assert cli._cmd_region("region home alpha") == " home is now alpha"
|
||||
assert cli._cmd_region("region home") == " home is alpha"
|
||||
|
||||
assert cli._cmd_region("region default") == " default scope is <null>"
|
||||
assert cli._cmd_region("region default alpha") == " default scope is now alpha"
|
||||
assert cfg["mesh"]["default_region"] == "alpha"
|
||||
assert cli._cmd_region("region default") == " default scope is alpha"
|
||||
assert cli._cmd_region("region default <null>") == " default scope is now <null>"
|
||||
assert cfg["mesh"]["default_region"] is None
|
||||
|
||||
assert cli._cmd_region("region put gamma alpha") == "OK - (flood allowed)"
|
||||
assert any(r[0] == "gamma" for r in created)
|
||||
|
||||
assert cli._cmd_region("region remove alpha") == "Err - not empty"
|
||||
assert cli._cmd_region("region remove gamma") == "OK"
|
||||
assert deleted
|
||||
|
||||
assert cli._cmd_region("region list denied") == "beta"
|
||||
listed = cli._cmd_region("region list allowed")
|
||||
assert listed.startswith("*,")
|
||||
assert "alpha" in listed
|
||||
assert cli._cmd_region("region list nope") == "Err - use 'allowed' or 'denied'"
|
||||
|
||||
|
||||
def test_discovery_auto_add_skips_local_node_and_persists_remote():
|
||||
identity = SimpleNamespace(get_public_key=lambda: bytes.fromhex("11" * 32))
|
||||
storage = SimpleNamespace(record_advert=MagicMock())
|
||||
cli = MeshCLI(
|
||||
"/tmp/cfg.yaml", _base_config(), _cfg_mgr(), identity=identity, storage_handler=storage
|
||||
)
|
||||
|
||||
self_result = cli._auto_add_discovery_result({"pub_key": "11" * 8, "node_type": 2})
|
||||
assert self_result["is_self"] is True
|
||||
storage.record_advert.assert_not_called()
|
||||
|
||||
remote_result = cli._auto_add_discovery_result(
|
||||
{
|
||||
"pub_key": "22" * 32,
|
||||
"node_name": "Remote Repeater",
|
||||
"node_type": 2,
|
||||
"rssi": -70,
|
||||
"response_snr": 4.25,
|
||||
}
|
||||
)
|
||||
assert remote_result["auto_added"] is True
|
||||
storage.record_advert.assert_called_once()
|
||||
|
||||
Reference in New Issue
Block a user