Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions plugins/platforms/buzz/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,7 @@ async def _handle_membership_event(self, websocket, subscriptions: Dict[str, Opt
subscribe to any new ones (fresh DMs dispatch from their beginning)."""
self._membership_since = max(self._membership_since, int(event.get("created_at") or 0))
before = set(self._channel_state)
await self._discover_joined_channels()
await self._discover_dms(seed=False)
for channel_id in self._channel_state:
if channel_id in before:
Expand Down Expand Up @@ -962,6 +963,32 @@ async def _discover_dms(self, *, seed: bool) -> None:
else:
self._channel_state[ch_id] = {"chat_type": "group", "last_ts": 0, "seen": OrderedDict()}

async def _discover_joined_channels(self) -> None:
"""Adopt newly joined community channels when no watch list is pinned.

The initial high-water mark suppresses pre-membership history, matching
startup channel discovery. This is deliberately separate from the DM
fallback: real channels must never be reclassified as DMs merely
because their membership changes.
"""
if self.channels:
return
code, out, err = await self._run_cli(["channels", "list"])
if code != 0:
logger.debug(
"Buzz: could not discover newly joined channels — %s", _cli_error_message(err, code)
)
return
for ch in _parse_json_list(out):
ch_id = str(ch.get("channel_id") or "")
if not ch_id:
continue
self._channel_meta[ch_id] = ch
self._channel_names.setdefault(ch_id, str(ch.get("name") or ch_id))
if ch_id in self._channel_state or self._may_reclassify_as_dm(ch_id):
continue
await self._seed_channel(ch_id, chat_type="group")

async def _poll_channel(self, channel_id: str) -> None:
state = self._channel_state.get(channel_id)
if state is None:
Expand Down
80 changes: 80 additions & 0 deletions tests/gateway/test_buzz_websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,83 @@ async def recv(self):
await adapter._authenticate_websocket(RejectingWs())



@pytest.mark.asyncio
async def test_membership_event_adopts_new_named_channel_when_unpinned():
"""A new community membership must subscribe without a gateway restart (#75107)."""
adapter = _make_adapter()
new_channel = "new-community-channel"

messages_get_calls = 0

async def run_cli(args, **_kwargs):
nonlocal messages_get_calls
if args == ["dms", "list"]:
return 0, json.dumps([{"dm_id": new_channel}]), ""
if args == ["channels", "list"]:
return 0, json.dumps([
{"channel_id": new_channel, "name": "release-team", "description": "Announcements"}
]), ""
if args[:2] == ["messages", "get"]:
messages_get_calls += 1
return 0, json.dumps([{"id": "pre-join", "created_at": 41}]), ""
raise AssertionError(f"unexpected CLI command: {args}")

class WebSocket:
def __init__(self):
self.sent = []

async def send(self, raw):
self.sent.append(json.loads(raw))

adapter._run_cli = run_cli
websocket = WebSocket()
subscriptions = {"membership": None}
await adapter._handle_membership_event(websocket, subscriptions, {"created_at": 42})

state = adapter._channel_state[new_channel]
assert state["chat_type"] == "group"
assert state["last_ts"] == 41
assert "pre-join" in state["seen"]
subscription_id = next(key for key, value in subscriptions.items() if value == new_channel)
assert websocket.sent == [[
"REQ", subscription_id, {"kinds": [9], "#h": [new_channel], "since": 40}
]]

await adapter._handle_membership_event(websocket, subscriptions, {"created_at": 43})
assert messages_get_calls == 1
assert len(websocket.sent) == 1

@pytest.mark.asyncio
async def test_membership_event_does_not_adopt_channel_outside_explicit_watch_list():
adapter = _make_adapter({"channels": [CHANNEL]})

async def run_cli(args, **_kwargs):
if args == ["dms", "list"]:
return 0, "[]", ""
if args == ["channels", "list"]:
return 0, json.dumps([
{"channel_id": "not-configured", "name": "release-team", "description": "Announcements"}
]), ""
raise AssertionError(f"unexpected CLI command: {args}")

adapter._run_cli = run_cli
await adapter._handle_membership_event(_FakeWebSocket(), {"membership": None}, {"created_at": 42})
assert "not-configured" not in adapter._channel_state

@pytest.mark.asyncio
async def test_membership_event_leaves_state_unchanged_when_channel_lookup_fails():
adapter = _make_adapter()

async def run_cli(args, **_kwargs):
if args == ["dms", "list"]:
return 0, "[]", ""
if args == ["channels", "list"]:
return 1, "", "unavailable"
raise AssertionError(f"unexpected CLI command: {args}")

adapter._run_cli = run_cli
subscriptions = {"membership": None}
await adapter._handle_membership_event(_FakeWebSocket(), subscriptions, {"created_at": 42})
assert adapter._channel_state == {}
assert subscriptions == {"membership": None}