From 8acba8394f8e582f7b8ea076a63cb66e2c0c7f54 Mon Sep 17 00:00:00 2001 From: TeTeHacko Date: Mon, 24 Aug 2026 00:05:30 +0200 Subject: [PATCH] fix(rooms): advertise settings.node_name from the CLI path too The room CLI advert read only settings.room_name while the daemon scheduler and the HTTP endpoint read settings.node_name -- so when the two keys differed, a single `advert` issued over the mesh CLI renamed the room on the mesh to the identity-name fallback. Both read sites now resolve node_name -> room_name -> identity name. Seen in production on 2026-08-19: a room configured with node_name only renamed itself after one CLI advert and clients tracked it under the identity name until the next scheduler advert. Co-Authored-By: Claude Fable 5 --- repeater/handler_helpers/room_server.py | 7 ++-- repeater/main.py | 2 +- tests/test_handler_helpers_room_server.py | 43 +++++++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/repeater/handler_helpers/room_server.py b/repeater/handler_helpers/room_server.py index 89a56e6..b15b464 100644 --- a/repeater/handler_helpers/room_server.py +++ b/repeater/handler_helpers/room_server.py @@ -145,8 +145,11 @@ class RoomServer: room_settings = rs.get("settings", {}) break - # Use room-specific name and location - node_name = room_settings.get("room_name", room_name) + # Use room-specific name and location. Prefer node_name to + # match the daemon scheduler and the HTTP endpoint — reading + # only room_name here meant one CLI advert renamed the room + # on the mesh to the identity-name fallback. + node_name = room_settings.get("node_name", room_settings.get("room_name", room_name)) latitude = room_settings.get("latitude", 0.0) longitude = room_settings.get("longitude", 0.0) diff --git a/repeater/main.py b/repeater/main.py index 3375935..bbf3ccd 100644 --- a/repeater/main.py +++ b/repeater/main.py @@ -1537,7 +1537,7 @@ class RepeaterDaemon: settings = ( identity_config.get("settings", {}) if isinstance(identity_config, dict) else {} ) - node_name = settings.get("node_name", room_name) + node_name = settings.get("node_name", settings.get("room_name", room_name)) latitude = settings.get("latitude", 0.0) longitude = settings.get("longitude", 0.0) flags = ADVERT_FLAG_IS_ROOM_SERVER | ADVERT_FLAG_HAS_NAME diff --git a/tests/test_handler_helpers_room_server.py b/tests/test_handler_helpers_room_server.py index 1dc8f03..7f02a76 100644 --- a/tests/test_handler_helpers_room_server.py +++ b/tests/test_handler_helpers_room_server.py @@ -234,6 +234,49 @@ async def test_room_server_send_advert_callback_returns_false_on_inject_failure( injector.assert_awaited_once() +@pytest.mark.asyncio +async def test_room_server_cli_advert_uses_node_name(): + """CLI advert must advertise settings.node_name, matching the scheduler. + + The CLI path read only settings.room_name while the daemon scheduler and + the HTTP endpoint read settings.node_name -- so a single `advert` from + the room CLI renamed the room on the mesh to the identity-name fallback + when the two keys differed. + """ + config = { + "identities": { + "room_servers": [ + { + "name": "room-alpha", + "settings": {"node_name": "Room Alpha", "latitude": 1.0, "longitude": 2.0}, + } + ] + } + } + + injector = AsyncMock(return_value=True) + rs = RoomServer( + room_hash=0x34, + room_name="room-alpha", + local_identity=_FakeIdentity(b"R" * 32), + sqlite_handler=_FakeDB(), + packet_injector=injector, + acl=_FakeACL(), + config_path="/tmp/room.yaml", + config=config, + config_manager=SimpleNamespace(), + ) + + packet = SimpleNamespace() + with patch( + "openhop_core.protocol.PacketBuilder.create_advert", return_value=packet + ) as create_advert: + ok = await rs.cli.send_advert_callback() + + assert ok is True + assert create_advert.call_args.kwargs.get("name") == "Room Alpha" + + def test_room_server_init_caps_max_posts_to_hard_limit(): rs = _make_room_server(max_posts=MAX_UNSYNCED_POSTS + 50) assert rs.max_posts == MAX_UNSYNCED_POSTS