fix(slack): resolve a cold channel's workspace from the sole authenticated team
scope_id_for_chat only consulted the channel→team map, which is empty right after boot (and after a reconnect) until an inbound event from that channel arrives. A /handoff into a Slack home without a stored scope_id (SLACK_HOME_CHANNEL env homes, or config homes never re-set via /sethome) therefore built a key without the team while every thread reply carries it — the handed-off thread was still orphaned across a restart (#111896). When the map has no entry and the channel is not known to be shared across workspaces, fall back to the single authenticated workspace (filled by auth.test at connect); multi-workspace installs keep returning None.
This commit is contained in:
@@ -1909,8 +1909,14 @@ class SlackAdapter(BasePlatformAdapter):
|
||||
def scope_id_for_chat(self, chat_id: str) -> Optional[str]:
|
||||
"""Return the workspace id owning ``chat_id``.
|
||||
``None`` for unknown channels and for channels claimed by several workspaces (dropped from
|
||||
the map) — no scope beats a wrong one."""
|
||||
the map) — no scope beats a wrong one. A channel unseen since boot (the map only fills from
|
||||
inbound events) still resolves when exactly one workspace is authenticated: every inbound
|
||||
reply will carry that team_id, so a caller keying a session on it must match (#111896)."""
|
||||
team_id = chat_id and (getattr(self, "_channel_team", None) or {}).get(str(chat_id))
|
||||
if not team_id and chat_id and str(chat_id) not in (getattr(self, "_channel_teams", None) or {}):
|
||||
team_clients = getattr(self, "_team_clients", None) or {}
|
||||
if len(team_clients) == 1:
|
||||
team_id = next(iter(team_clients))
|
||||
return str(team_id) if team_id else None
|
||||
|
||||
def _get_client(self, chat_id: str, team_id: Optional[str] = None) -> Any:
|
||||
|
||||
@@ -104,24 +104,27 @@ def test_discord_handoff_key_does_not_use_parent_channel():
|
||||
assert handoff != buggy, "handoff regressed to keying on the parent channel"
|
||||
|
||||
|
||||
def _slack_handoff_destination(channel_id: str, thread_ts: str, team_id: str):
|
||||
"""Run the real handoff destination/key path against a Slack home channel."""
|
||||
config = GatewayConfig(platforms={Platform.SLACK: PlatformConfig(enabled=True, token="test")})
|
||||
config.platforms[Platform.SLACK].home_channel = HomeChannel(
|
||||
platform=Platform.SLACK, chat_id=channel_id, name="home", scope_id=team_id)
|
||||
adapter = MagicMock()
|
||||
adapter.create_handoff_thread = AsyncMock(return_value=thread_ts)
|
||||
def _handoff_destination(platform: Platform, channel_id: str, thread_id: str, scope_id, adapter):
|
||||
"""Run the real handoff destination/key path against a configured home channel."""
|
||||
config = GatewayConfig(platforms={platform: PlatformConfig(enabled=True, token="test")})
|
||||
config.platforms[platform].home_channel = HomeChannel(
|
||||
platform=platform, chat_id=channel_id, name="home", scope_id=scope_id)
|
||||
adapter.create_handoff_thread = AsyncMock(return_value=thread_id)
|
||||
runner = object.__new__(GatewayRunner)
|
||||
runner.config = config
|
||||
runner.adapters = {Platform.SLACK: adapter}
|
||||
runner.adapters = {platform: adapter}
|
||||
runner.session_store = None
|
||||
with patch("gateway.delivery.resolve_delivery_transport",
|
||||
lambda *_a: SimpleNamespace(adapter=adapter, send=AsyncMock())):
|
||||
dest = asyncio.run(runner._handoff_resolve_destination(
|
||||
{"id": "cli-session", "title": "work", "handoff_platform": "slack"}, profile_name=None))
|
||||
{"id": "cli-session", "title": "work", "handoff_platform": platform.value}, profile_name=None))
|
||||
return dest, runner._handoff_session_key(dest, profile_name=None)
|
||||
|
||||
|
||||
def _slack_handoff_destination(channel_id: str, thread_ts: str, team_id: str):
|
||||
return _handoff_destination(Platform.SLACK, channel_id, thread_ts, team_id, MagicMock())
|
||||
|
||||
|
||||
def _organic_slack_reply_key(channel_id: str, thread_ts: str, team_id: str, chat_type: str) -> str:
|
||||
"""Key the Slack adapter builds for a thread reply (``_build_message_event``): parent channel
|
||||
as chat_id, ``dm``/``group`` from the channel type, workspace id as scope_id."""
|
||||
@@ -144,3 +147,14 @@ def test_slack_channel_handoff_key_matches_the_thread_reply_key():
|
||||
dest, handoff = _slack_handoff_destination("C0CHANNEL01", "1789474088.089709", "T0C2HL96FH6")
|
||||
assert handoff == _organic_slack_reply_key("C0CHANNEL01", "1789474088.089709", "T0C2HL96FH6", "group")
|
||||
assert dest.source.chat_id == "C0CHANNEL01"
|
||||
|
||||
|
||||
def test_slack_handoff_without_stored_scope_uses_the_sole_workspace_on_a_cold_channel_map():
|
||||
"""An env/legacy home has no scope_id and the adapter's channel→team map is empty right after
|
||||
boot; a single authenticated workspace still identifies the team the reply key will carry."""
|
||||
from plugins.platforms.slack.adapter import SlackAdapter
|
||||
|
||||
adapter = SlackAdapter.__new__(SlackAdapter)
|
||||
adapter._channel_team, adapter._channel_teams, adapter._team_clients = {}, {}, {"T0C2HL96FH6": object()}
|
||||
_dest, handoff = _handoff_destination(Platform.SLACK, "D0C1HFBMQAX", "1789474088.089709", None, adapter)
|
||||
assert handoff == _organic_slack_reply_key("D0C1HFBMQAX", "1789474088.089709", "T0C2HL96FH6", "dm")
|
||||
|
||||
Reference in New Issue
Block a user