From 729bbb7a309a3d13d8cc7d1cd2fbab79e7d969f7 Mon Sep 17 00:00:00 2001 From: Ben Barclay Date: Wed, 1 Jul 2026 12:30:59 +1000 Subject: [PATCH] refactor(relay): purge platform-specific scope terminology from the relay adapter (D-Q2.5c) (#56016) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gateway HALF of the D-Q2.5c cleanup (connector half: gateway-gateway #92). Scope is STRICTLY the relay adapter (gateway/relay/) — session.py and every native platform adapter are untouched (SessionSource.guild_id remains for their use; it is NOT relay-only). Within gateway/relay/, drop the D-Q2.5 wire dual-write/dual-read alias AND genericize all platform-specific (Discord "guild") scope terminology: - ws_transport._event_from_wire: read scope_id only (drop the ?? guild_id fallback). - adapter._with_scope: emit scope_id only on outbound metadata (drop the guild_id dual-write); genericize the "GUILD reply" docstring to "SCOPED reply". - adapter._capture_scope: read source.scope_id only; rename the local `guild` var to `scope`; genericize the docstring + the _scope_by_chat/_dm_user_by_chat field comments ("guild_id (Discord)" -> "scope_id (server/workspace scope)"). - __init__.relay_route_keys docstring: "guild_ids" -> "scope_ids". - The ONE real Discord `guild_id` kept: the raw inbound interaction payload field (payload.get("guild_id")), which is Discord's own wire field, mapped straight into the generic scope_id slot — unchanged. Contract doc (docs/relay-connector-contract.md): reframe the `guild_id` row as a legacy alias the connector no longer reads (session.py's agent-wide to_dict() still emits it for non-relay persistence, so it stays documented + wire-present but ignored) — accurate, and keeps the to_dict()-vs-doc conformance test green. Tests (relay only): migrate the wire-key writes + assertions guild_id -> scope_id across test_relay_adapter / _ws_transport / _passthrough / _roundtrip / _roundtrip_telegram / _multiplatform; keep raw Discord `type:2` interaction payloads' guild_id (real Discord field) and the conformance test's guild_id parametrize (validates the kept legacy field stays wire-reachable). Gate: 156 relay tests pass, ruff clean. Cross-repo E2E — all 14 drivers pass BOTH ways: connector#92 (scope_id-only) x agent-main (still dual-reads) AND connector#92 x this worktree (scope_id-only). Deploy-order-safe either way. --- docs/relay-connector-contract.md | 4 +- gateway/relay/__init__.py | 2 +- gateway/relay/adapter.py | 42 ++++++-------- gateway/relay/ws_transport.py | 3 +- tests/gateway/relay/test_relay_adapter.py | 58 +++++++++---------- .../gateway/relay/test_relay_multiplatform.py | 2 +- tests/gateway/relay/test_relay_passthrough.py | 4 +- tests/gateway/relay/test_relay_roundtrip.py | 12 ++-- .../relay/test_relay_roundtrip_telegram.py | 14 ++--- tests/gateway/relay/test_ws_transport.py | 4 +- 10 files changed, 70 insertions(+), 75 deletions(-) diff --git a/docs/relay-connector-contract.md b/docs/relay-connector-contract.md index 8c45c401d..30646bf59 100644 --- a/docs/relay-connector-contract.md +++ b/docs/relay-connector-contract.md @@ -157,7 +157,7 @@ present (may be `null`); the rest are included only when set. | `user_id_alt` | string | no | Platform-specific stable alt id (Signal UUID, Feishu union_id). | | `chat_id_alt` | string | no | Alternate chat id (e.g. Signal group internal id). | | `scope_id` | string | no | Platform-neutral **scope** discriminator: Discord guild / Slack workspace / Matrix server. **REQUIRED for Discord/Slack scope isolation.** Session-key discriminator. (Canonical name as of the D-Q2.5 wire migration.) | -| `guild_id` | string | no | **Deprecated alias for `scope_id`** — still emitted and read during the cross-repo dual-read/dual-write overlap; readers resolve `scope_id ?? guild_id`. Dropped once both repos deploy on `scope_id`. | +| `guild_id` | string | no | **Legacy alias, no longer read by the connector.** As of D-Q2.5c the connector reads and writes only `scope_id`; the gateway's agent-wide `SessionSource.to_dict()` still emits `guild_id` (mirrored to `scope_id`) for non-relay session persistence, so it may still appear on the wire but the connector ignores it. Do not depend on it. | | `parent_chat_id` | string | no | Parent channel when `chat_id` refers to a thread. | | `message_id` | string | no | Id of the triggering message (for pin/reply/react). | @@ -168,7 +168,7 @@ present (may be `null`); the rest are included only when set. ### SessionSource discriminators per platform -| Platform | chat_id | chat_type | user_id | thread_id | guild_id | +| Platform | chat_id | chat_type | user_id | thread_id | scope_id | | --- | --- | --- | --- | --- | --- | | **Discord** | channel id | `dm`/`group`/`thread` | author id | thread channel id (threads) | **guild id** (REQUIRED for server isolation) | | **Telegram** | chat id | `dm`/`group`/`forum` | from id | forum topic id (forums) | — | diff --git a/gateway/relay/__init__.py b/gateway/relay/__init__.py index 7416dcdc0..74f8858ad 100644 --- a/gateway/relay/__init__.py +++ b/gateway/relay/__init__.py @@ -172,7 +172,7 @@ def relay_endpoint() -> Optional[str]: def relay_route_keys() -> list[str]: - """Discriminators (guild_ids / chat_ids / paths) this gateway's tenant owns. + """Discriminators (scope_ids / chat_ids / paths) this gateway's tenant owns. Gateway-provided config, paired with ``relay_endpoint()``: the connector writes one route row per (routeKey -> tenant, endpoint), so route keys only diff --git a/gateway/relay/adapter.py b/gateway/relay/adapter.py index 3dc81d9ec..34cc51522 100644 --- a/gateway/relay/adapter.py +++ b/gateway/relay/adapter.py @@ -59,15 +59,15 @@ class RelayAdapter(BasePlatformAdapter): self._transport = transport # Capability surface read by stream_consumer (getattr(..., 4096)). self.MAX_MESSAGE_LENGTH = descriptor.max_message_length - # chat_id -> guild_id (Discord) / workspace scope, learned from inbound + # chat_id -> scope_id (server/workspace scope), learned from inbound # events. The connector's egress guard resolves the owning tenant from - # the OUTBOUND action's metadata.guild_id; the gateway's generic delivery + # the OUTBOUND action's metadata.scope_id; the gateway's generic delivery # path (run.py _thread_metadata_for_source) only carries thread_id, so we # re-attach the scope here from what we saw inbound. Keyed by chat_id # (channel) since that's what send() receives. See routedEgressGuard.ts. self._scope_by_chat: Dict[str, str] = {} - # chat_id -> author user_id for DM channels (no guild_id). A DM reply has - # no guild discriminator, so the connector resolves its tenant from the + # chat_id -> author user_id for DM chats (no scope). A DM reply has + # no scope discriminator, so the connector resolves its tenant from the # recipient's author binding; we re-attach this user_id as # metadata.user_id on the outbound action so it can. See _capture_scope. self._dm_user_by_chat: Dict[str, str] = {} @@ -235,15 +235,15 @@ class RelayAdapter(BasePlatformAdapter): tenant resolution. Never raises — scope tracking must not break inbound. Two cases, matching the connector's two tenant-resolution paths: - - GUILD message: remember chat_id -> guild_id. The connector resolves - the tenant from metadata.guild_id (routing table). - - DM (no guild_id): remember chat_id -> the authentic author user_id. - A DM carries no guild discriminator, so the connector instead resolves + - SCOPED message: remember chat_id -> scope_id. The connector resolves + the tenant from metadata.scope_id (routing table). + - DM (no scope): remember chat_id -> the authentic author user_id. + A DM carries no scope discriminator, so the connector instead resolves the tenant from the recipient's author binding (resolveByUser); it needs the user_id on the OUTBOUND action to do that. Without this, a DM reply has no resolvable discriminator and the connector's egress guard declines it as "target not routed to an onboarded tenant". - See gateway-gateway routedEgressGuard.ts / discordTenantOf. + See gateway-gateway routedEgressGuard.ts / the tenant resolvers. """ try: src = getattr(event, "source", None) @@ -263,9 +263,9 @@ class RelayAdapter(BasePlatformAdapter): platform_value = getattr(platform, "value", platform) if platform_value and platform_value != "relay": self._platform_by_chat[str(chat)] = str(platform_value) - guild = getattr(src, "scope_id", None) or getattr(src, "guild_id", None) - if guild: - self._scope_by_chat[str(chat)] = str(guild) + scope = getattr(src, "scope_id", None) + if scope: + self._scope_by_chat[str(chat)] = str(scope) return # DM: no scope. Remember the authentic author id for outbound # author-binding resolution (the user we're replying to in this DM). @@ -279,28 +279,24 @@ class RelayAdapter(BasePlatformAdapter): """Ensure the outbound metadata carries the discriminator the connector's egress guard needs to resolve the owning tenant. Two cases: - - GUILD reply: re-attach metadata.scope_id (routing-table resolution; - also mirrored to the deprecated metadata.guild_id during the D-Q2.5 - wire migration so a connector on either side resolves the tenant). + - SCOPED reply: re-attach metadata.scope_id (routing-table resolution). - DM reply: there is no scope, so re-attach metadata.user_id — the authentic author id we saw inbound — which the connector resolves to the tenant via the recipient's author binding (resolveByUser). Without one of these, egress is declined as 'target not routed to an onboarded - tenant'. See gateway-gateway routedEgressGuard.ts / discordTenantOf. + tenant'. See gateway-gateway routedEgressGuard.ts / the tenant resolvers. No-op when the relevant value is already present or unknown for this chat. """ meta: Dict[str, Any] = dict(metadata or {}) - if not meta.get("scope_id") and not meta.get("guild_id"): + if not meta.get("scope_id"): scope = self._scope_by_chat.get(str(chat_id)) if scope: - # D-Q2.5 dual-write: canonical scope_id + deprecated guild_id alias. meta["scope_id"] = scope - meta["guild_id"] = scope # DM author-binding discriminator. Only meaningful when there's no scope - # (a guild reply resolves by scope_id); harmless to carry otherwise, but + # (a scoped reply resolves by scope_id); harmless to carry otherwise, but # we only set it when this chat is a known DM and the field is absent. - if not meta.get("scope_id") and not meta.get("guild_id") and not meta.get("user_id"): + if not meta.get("scope_id") and not meta.get("user_id"): dm_user = self._dm_user_by_chat.get(str(chat_id)) if dm_user: meta["user_id"] = dm_user @@ -405,14 +401,14 @@ class RelayAdapter(BasePlatformAdapter): member = payload.get("member") or {} user = (member.get("user") if isinstance(member, dict) else None) or payload.get("user") or {} channel_id = str(payload.get("channel_id") or "") - guild_id = payload.get("guild_id") # real Discord interaction field + guild_id = payload.get("guild_id") # real Discord interaction wire field source = SessionSource( platform=Platform.RELAY, chat_id=channel_id, chat_type="channel" if guild_id else "dm", user_id=str(user.get("id")) if isinstance(user, dict) and user.get("id") else None, user_name=str(user.get("username")) if isinstance(user, dict) and user.get("username") else None, - scope_id=str(guild_id) if guild_id else None, # Discord guild → generic scope slot (D-Q2.5) + scope_id=str(guild_id) if guild_id else None, # Discord guild → generic scope slot message_id=str(payload.get("id")) if payload.get("id") else None, ) return MessageEvent(text=text, message_type=MessageType.TEXT, source=source) diff --git a/gateway/relay/ws_transport.py b/gateway/relay/ws_transport.py index 24055072f..2f79222a4 100644 --- a/gateway/relay/ws_transport.py +++ b/gateway/relay/ws_transport.py @@ -117,8 +117,7 @@ def _event_from_wire(raw: Dict[str, Any]) -> MessageEvent: chat_topic=src.get("chat_topic"), user_id_alt=src.get("user_id_alt"), chat_id_alt=src.get("chat_id_alt"), - # D-Q2.5 dual-read: prefer canonical scope_id, fall back to legacy guild_id. - scope_id=src.get("scope_id", src.get("guild_id")), + scope_id=src.get("scope_id"), parent_chat_id=src.get("parent_chat_id"), message_id=src.get("message_id"), # Authentic upstream-trust signal: this event arrived over the diff --git a/tests/gateway/relay/test_relay_adapter.py b/tests/gateway/relay/test_relay_adapter.py index cc8376246..91d38edd4 100644 --- a/tests/gateway/relay/test_relay_adapter.py +++ b/tests/gateway/relay/test_relay_adapter.py @@ -129,7 +129,7 @@ class _CaptureTransport: return {"success": True, "message_id": "m1"} -def _make_event(chat_id="chan-1", guild_id="guild-9"): +def _make_event(chat_id="chan-1", scope_id="scope-9"): from gateway.platforms.base import MessageEvent, MessageType from gateway.session import SessionSource @@ -137,13 +137,13 @@ def _make_event(chat_id="chan-1", guild_id="guild-9"): platform=Platform.RELAY, chat_id=chat_id, chat_type="channel", - guild_id=guild_id, + scope_id=scope_id, ) return MessageEvent(text="hi", source=src, message_type=MessageType.TEXT) def _make_dm_event(chat_id="dm-1", user_id="user-42"): - """An inbound DM: no guild_id, carries the authentic author user_id.""" + """An inbound DM: no scope_id, carries the authentic author user_id.""" from gateway.platforms.base import MessageEvent, MessageType from gateway.session import SessionSource @@ -151,53 +151,53 @@ def _make_dm_event(chat_id="dm-1", user_id="user-42"): platform=Platform.RELAY, chat_id=chat_id, chat_type="dm", - guild_id=None, + scope_id=None, user_id=user_id, ) return MessageEvent(text="hi", source=src, message_type=MessageType.TEXT) @pytest.mark.asyncio -async def test_send_reattaches_guild_id_from_inbound_scope(): +async def test_send_reattaches_scope_id_from_inbound_scope(): """The connector's egress guard resolves the owning tenant from - metadata.guild_id; the gateway's generic delivery path drops it, so the - relay adapter must re-attach the guild scope learned from the inbound event. - Regression for live 'discord egress declined: target not routed to an + metadata.scope_id; the gateway's generic delivery path drops it, so the + relay adapter must re-attach the scope learned from the inbound event. + Regression for live 'egress declined: target not routed to an onboarded tenant'.""" t = _CaptureTransport() a = RelayAdapter(PlatformConfig(), make_desc(platform="discord"), transport=t) - # Simulate the connector delivering an inbound message in guild-9 / chan-1, + # Simulate the connector delivering an inbound message in scope-9 / chan-1, # but don't run the full handle_message pipeline — just the scope capture. - a._capture_scope(_make_event(chat_id="chan-1", guild_id="guild-9")) + a._capture_scope(_make_event(chat_id="chan-1", scope_id="scope-9")) await a.send("chan-1", "the reply") - assert t.sent["metadata"].get("guild_id") == "guild-9" + assert t.sent["metadata"].get("scope_id") == "scope-9" @pytest.mark.asyncio -async def test_send_without_known_scope_omits_guild_id(): - """A chat we never saw inbound (e.g. a DM) gets no guild_id — no-op, never +async def test_send_without_known_scope_omits_scope_id(): + """A chat we never saw inbound (e.g. a DM) gets no scope_id — no-op, never invents a scope.""" t = _CaptureTransport() a = RelayAdapter(PlatformConfig(), make_desc(platform="discord"), transport=t) await a.send("unknown-chat", "hi") - assert "guild_id" not in t.sent["metadata"] + assert "scope_id" not in t.sent["metadata"] @pytest.mark.asyncio -async def test_send_preserves_explicit_guild_id(): - """An explicitly-provided metadata.guild_id is never overwritten.""" +async def test_send_preserves_explicit_scope_id(): + """An explicitly-provided metadata.scope_id is never overwritten.""" t = _CaptureTransport() a = RelayAdapter(PlatformConfig(), make_desc(platform="discord"), transport=t) - a._capture_scope(_make_event(chat_id="chan-1", guild_id="guild-9")) - await a.send("chan-1", "hi", metadata={"guild_id": "explicit-1"}) - assert t.sent["metadata"]["guild_id"] == "explicit-1" + a._capture_scope(_make_event(chat_id="chan-1", scope_id="scope-9")) + await a.send("chan-1", "hi", metadata={"scope_id": "explicit-1"}) + assert t.sent["metadata"]["scope_id"] == "explicit-1" @pytest.mark.asyncio async def test_send_reattaches_dm_user_id_from_inbound_scope(): - """A DM reply has no guild_id, so the connector resolves the tenant from the + """A DM reply has no scope_id, so the connector resolves the tenant from the recipient's author binding — it needs metadata.user_id. The adapter must re-attach the authentic author id learned from the inbound DM. Regression for live 'discord egress declined: target not routed to an onboarded tenant' on @@ -209,8 +209,8 @@ async def test_send_reattaches_dm_user_id_from_inbound_scope(): await a.send("dm-1", "the reply") assert t.sent["metadata"].get("user_id") == "user-42" - # A DM carries no guild_id — only the author discriminator. - assert "guild_id" not in t.sent["metadata"] + # A DM carries no scope_id — only the author discriminator. + assert "scope_id" not in t.sent["metadata"] @pytest.mark.asyncio @@ -220,7 +220,7 @@ async def test_send_dm_does_not_invent_user_id_for_unknown_chat(): a = RelayAdapter(PlatformConfig(), make_desc(platform="discord"), transport=t) await a.send("unknown-dm", "hi") assert "user_id" not in t.sent["metadata"] - assert "guild_id" not in t.sent["metadata"] + assert "scope_id" not in t.sent["metadata"] @pytest.mark.asyncio @@ -234,15 +234,15 @@ async def test_send_preserves_explicit_user_id(): @pytest.mark.asyncio -async def test_guild_reply_does_not_carry_user_id(): - """A guild reply resolves by guild_id and must NOT carry a DM user_id even if - the same chat_id was somehow seen — guild capture wins and user_id stays out - (guild_id is the discriminator; user_id is the DM-only fallback).""" +async def test_scoped_reply_does_not_carry_user_id(): + """A scoped reply resolves by scope_id and must NOT carry a DM user_id even if + the same chat_id was somehow seen — scope capture wins and user_id stays out + (scope_id is the discriminator; user_id is the DM-only fallback).""" t = _CaptureTransport() a = RelayAdapter(PlatformConfig(), make_desc(platform="discord"), transport=t) - a._capture_scope(_make_event(chat_id="chan-1", guild_id="guild-9")) + a._capture_scope(_make_event(chat_id="chan-1", scope_id="scope-9")) await a.send("chan-1", "hi") - assert t.sent["metadata"].get("guild_id") == "guild-9" + assert t.sent["metadata"].get("scope_id") == "scope-9" assert "user_id" not in t.sent["metadata"] diff --git a/tests/gateway/relay/test_relay_multiplatform.py b/tests/gateway/relay/test_relay_multiplatform.py index 06fd47e73..a7b975e9c 100644 --- a/tests/gateway/relay/test_relay_multiplatform.py +++ b/tests/gateway/relay/test_relay_multiplatform.py @@ -194,7 +194,7 @@ async def test_adapter_stamps_per_frame_platform_from_inbound(monkeypatch): MessageEvent( text="yo", message_type=MessageType.TEXT, - source=SessionSource(platform=Platform.DISCORD, chat_id="dc-1", chat_type="channel", guild_id="g-1"), + source=SessionSource(platform=Platform.DISCORD, chat_id="dc-1", chat_type="channel", scope_id="g-1"), ) ) await adapter.send("dc-1", "a discord reply") diff --git a/tests/gateway/relay/test_relay_passthrough.py b/tests/gateway/relay/test_relay_passthrough.py index 51c5b8ee2..f59a6d6ae 100644 --- a/tests/gateway/relay/test_relay_passthrough.py +++ b/tests/gateway/relay/test_relay_passthrough.py @@ -120,10 +120,10 @@ async def test_discord_interaction_routes_through_handle_message(adapter, monkey ev = seen[0] assert ev.text == "summarize" assert ev.source.chat_id == "chan-9" - assert ev.source.guild_id == "guild-7" + assert ev.source.scope_id == "guild-7" assert ev.source.user_id == "user-3" assert ev.source.chat_type == "channel" - # Scope captured so the agent's reply re-asserts guild_id for egress. + # Scope captured so the agent's reply re-asserts scope_id for egress. assert adapter._scope_by_chat.get("chan-9") == "guild-7" diff --git a/tests/gateway/relay/test_relay_roundtrip.py b/tests/gateway/relay/test_relay_roundtrip.py index 2336d53ee..7509ba6c1 100644 --- a/tests/gateway/relay/test_relay_roundtrip.py +++ b/tests/gateway/relay/test_relay_roundtrip.py @@ -3,7 +3,7 @@ Proves the gateway side of the relay works with no real connector: - connect() registers the inbound handler, - a connector-delivered MessageEvent reaches the adapter's message path, - - SessionSource discriminators (guild_id) drive build_session_key isolation, + - SessionSource discriminators (scope_id) drive build_session_key isolation, - an outbound send round-trips through the transport. These target the transport contract + session-key derivation (Task 1.2's gate), @@ -40,14 +40,14 @@ def _discord_descriptor() -> CapabilityDescriptor: ) -def _discord_event(guild_id: str, channel_id: str, user_id: str, text: str) -> MessageEvent: +def _discord_event(scope_id: str, channel_id: str, user_id: str, text: str) -> MessageEvent: """Synthetic inbound the connector would build from a discord.js message.""" source = SessionSource( platform=Platform.DISCORD, chat_id=channel_id, chat_type="group", user_id=user_id, - guild_id=guild_id, + scope_id=scope_id, ) return MessageEvent(text=text, message_type=MessageType.TEXT, source=source) @@ -79,18 +79,18 @@ async def test_inbound_event_reaches_adapter(wired, monkeypatch): await stub.push_inbound(ev) assert len(captured) == 1 assert captured[0].text == "hello" - assert captured[0].source.guild_id == "guildA" + assert captured[0].source.scope_id == "guildA" @pytest.mark.asyncio -async def test_two_guilds_isolate_into_distinct_session_keys(wired): +async def test_two_scopes_isolate_into_distinct_session_keys(wired): adapter, _ = wired ev_a = _discord_event("guildA", "chan1", "userX", "hi from A") ev_b = _discord_event("guildB", "chan2", "userX", "hi from B") key_a = build_session_key(ev_a.source) key_b = build_session_key(ev_b.source) assert key_a != key_b - # Same guild + channel + user collapses to one session. + # Same scope + channel + user collapses to one session. ev_a2 = _discord_event("guildA", "chan1", "userX", "again") assert build_session_key(ev_a2.source) == key_a diff --git a/tests/gateway/relay/test_relay_roundtrip_telegram.py b/tests/gateway/relay/test_relay_roundtrip_telegram.py index 2efd822fc..9b95244b2 100644 --- a/tests/gateway/relay/test_relay_roundtrip_telegram.py +++ b/tests/gateway/relay/test_relay_roundtrip_telegram.py @@ -6,9 +6,9 @@ descriptors to round-trip and their inbound ``MessageEvent``s to drive ``build_session_key()`` correctly. Telegram's discriminator profile differs from Discord's, which is the point: - - No ``guild_id``; isolation between chats comes from ``chat_id`` alone. + - No ``scope_id``; isolation between chats comes from ``chat_id`` alone. - Forum topics live inside ONE ``chat_id`` and isolate by ``thread_id`` (the - Telegram analog of Discord's per-guild isolation). + Telegram analog of Discord's per-scope isolation). - Forum/thread sessions are shared across participants by default (``thread_sessions_per_user=False``) — user_id is NOT appended in a thread. - ``len_unit="utf16"`` (Telegram counts UTF-16 code units) and @@ -51,7 +51,7 @@ def _tg_group_event(chat_id: str, user_id: str, text: str, thread_id: str | None """Synthetic inbound the connector would build from a Telegram update. A plain group message has no thread_id; a forum-topic message carries the - topic id as thread_id (no guild_id — Telegram has no guild concept). + topic id as thread_id (no scope_id — Telegram has no scope concept). """ source = SessionSource( platform=Platform.TELEGRAM, @@ -105,13 +105,13 @@ async def test_inbound_telegram_event_reaches_adapter(wired, monkeypatch): assert len(captured) == 1 assert captured[0].text == "hello" assert captured[0].source.platform == Platform.TELEGRAM - assert captured[0].source.guild_id is None # Telegram has no guild + assert captured[0].source.scope_id is None # Telegram has no scope @pytest.mark.asyncio async def test_two_telegram_chats_isolate_by_chat_id(wired): - """No guild_id on Telegram — two distinct chats must still isolate, keyed - on chat_id alone (the Discord-guild role is played by chat_id here).""" + """No scope_id on Telegram — two distinct chats must still isolate, keyed + on chat_id alone (the Discord-scope role is played by chat_id here).""" ev_a = _tg_group_event("chat-A", "userX", "hi A") ev_b = _tg_group_event("chat-B", "userX", "hi B") key_a = build_session_key(ev_a.source) @@ -125,7 +125,7 @@ async def test_two_telegram_chats_isolate_by_chat_id(wired): @pytest.mark.asyncio async def test_forum_topics_isolate_by_thread_id_within_one_chat(wired): """Telegram forum topics share a single chat_id and isolate by thread_id — - the Telegram analog of Discord per-guild isolation. Two topics in the same + the Telegram analog of Discord per-scope isolation. Two topics in the same forum must NOT collide, and (threads shared across participants by default) a second user in the same topic shares the session.""" topic1 = _tg_group_event("forum-1", "userX", "in topic 1", thread_id="t-1") diff --git a/tests/gateway/relay/test_ws_transport.py b/tests/gateway/relay/test_ws_transport.py index 1a38aa9a7..22aa8949d 100644 --- a/tests/gateway/relay/test_ws_transport.py +++ b/tests/gateway/relay/test_ws_transport.py @@ -117,7 +117,7 @@ async def test_inbound_frame_reaches_handler(server): "event": { "text": "hello from connector", "message_type": "text", - "source": {"platform": "discord", "chat_id": "chan1", "chat_type": "group", "guild_id": "guildA"}, + "source": {"platform": "discord", "chat_id": "chan1", "chat_type": "group", "scope_id": "guildA"}, }, "bufferId": "buf-1", } @@ -132,7 +132,7 @@ async def test_inbound_frame_reaches_handler(server): await asyncio.sleep(0.05) assert len(received) == 1 assert received[0].text == "hello from connector" - assert received[0].source.guild_id == "guildA" + assert received[0].source.scope_id == "guildA" finally: await t.disconnect()